138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
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<string, string> = {
|
|
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;
|