import express from 'express'; import cors from 'cors'; import authRouter from './routes/auth.js'; import usersRouter from './routes/users.js'; import forumRouter from './routes/forum.js'; import bugsRouter from './routes/bugs.js'; import feedRouter from './routes/feed.js'; import eventsRouter from './routes/events.js'; import teamRouter from './routes/team.js'; import settingsRouter from './routes/settings.js'; const app = express(); // ── Logger ───────────────────────────────────────────────────────────────────── const R = '\x1b[0m'; const BOLD = '\x1b[1m'; const DIM = '\x1b[2m'; const RED = '\x1b[31m'; const GREEN = '\x1b[32m'; const YELLOW = '\x1b[33m'; const BLUE = '\x1b[34m'; const MAGENTA = '\x1b[35m'; const CYAN = '\x1b[36m'; const WHITE = '\x1b[37m'; const BG_RED = '\x1b[41m'; const BG_GREEN = '\x1b[42m'; const BG_YELLOW = '\x1b[43m'; const BG_BLUE = '\x1b[44m'; const BG_MAGENTA = '\x1b[45m'; const BG_CYAN = '\x1b[46m'; const METHOD_STYLE: Record = { GET: `${BG_BLUE}${WHITE}${BOLD}`, POST: `${BG_GREEN}${WHITE}${BOLD}`, PUT: `${BG_YELLOW}${WHITE}${BOLD}`, PATCH: `${BG_MAGENTA}${WHITE}${BOLD}`, DELETE: `${BG_RED}${WHITE}${BOLD}`, }; function methodBadge(method: string): string { const style = METHOD_STYLE[method] ?? `${BG_CYAN}${WHITE}${BOLD}`; return `${style} ${method.padEnd(6)} ${R}`; } function statusBadge(code: number): string { if (code < 300) return `${BG_GREEN}${WHITE}${BOLD} ${code} ${R}`; if (code < 400) return `${BG_YELLOW}${WHITE}${BOLD} ${code} ${R}`; return `${BG_RED}${WHITE}${BOLD} ${code} ${R}`; } function prettyJson(value: unknown): string { return JSON.stringify(value, (k, v) => k === 'password' ? '***' : v, 2) .split('\n') .map((line, i) => i === 0 ? line : ` ${DIM} ${line}${R}`) .join('\n'); } const SEP = `${DIM}${'─'.repeat(60)}${R}`; app.use((req, res, next) => { const start = Date.now(); const ts = new Date().toISOString().replace('T', ' ').slice(0, 23); // Skip health check noise if (req.originalUrl === '/api/health') { next(); return; } const originalJson = res.json.bind(res); let resBody: unknown; res.json = (body) => { resBody = body; return originalJson(body); }; res.on('finish', () => { const ms = Date.now() - start; const userId = req.user?.userId ? `${CYAN}${req.user.userId.slice(0, 8)}…${R}` : `${DIM}anon${R}`; const role = req.user?.role ? `${MAGENTA}${req.user.role}${R}` : `${DIM}-${R}`; const hasBody = ['POST', 'PUT', 'PATCH'].includes(req.method) && req.body && Object.keys(req.body).length > 0; const lines: string[] = [ SEP, `${DIM}${ts}${R} ${methodBadge(req.method)} ${BOLD}${req.originalUrl}${R}`, ` ${DIM}┌ user ${R} ${userId} ${DIM}role:${R} ${role}`, ` ${DIM}└ status ${R} ${statusBadge(res.statusCode)} ${DIM}${ms}ms${R}`, ]; if (hasBody) { lines.push(` ${GREEN}↑ REQUEST BODY${R}`); lines.push(` ${DIM} ${prettyJson(req.body)}${R}`); } if (res.statusCode >= 400 && resBody) { lines.push(` ${RED}↓ ERROR RESPONSE${R}`); lines.push(` ${DIM} ${prettyJson(resBody)}${R}`); } else if (res.statusCode < 300 && resBody && req.method !== 'GET') { lines.push(` ${GREEN}↓ RESPONSE BODY${R}`); lines.push(` ${DIM} ${prettyJson(resBody)}${R}`); } console.log(lines.join('\n')); }); next(); }); app.use(cors({ origin: [ 'http://localhost:5173', // nest-front dev 'http://localhost:5174', // nest-intra dev process.env.FRONT_ORIGIN ?? '', process.env.INTRA_ORIGIN ?? '', ].filter(Boolean), credentials: true, })); app.use(express.json()); app.get('/api/health', (_req, res) => res.json({ ok: true })); app.use('/api/auth', authRouter); app.use('/api/users', usersRouter); app.use('/api/forum', forumRouter); app.use('/api/bugs', bugsRouter); app.use('/api/feed', feedRouter); app.use('/api/events', eventsRouter); app.use('/api/team', teamRouter); app.use('/api/settings', settingsRouter); // 404 app.use((_req, res) => res.status(404).json({ error: 'Not found' })); // Global error handler app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { console.error(`${BG_RED}${WHITE}${BOLD} UNHANDLED ERROR ${R}`, err); res.status(500).json({ error: 'Internal server error' }); }); export default app;