68 lines
3.0 KiB
TypeScript
68 lines
3.0 KiB
TypeScript
import express, { type Express, type NextFunction, type Request, type Response } from 'express';
|
|
import cookieParser from 'cookie-parser';
|
|
import { existsSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { ZodError } from 'zod';
|
|
import type { Db } from './db/client.js';
|
|
import { ApiError } from './lib/errors.js';
|
|
import { currentUserOrNull, requireAuth, requireRole } from './middleware/auth.js';
|
|
import { ensureCsrfToken, verifyCsrf } from './middleware/csrf.js';
|
|
import { authRouter } from './routes/auth.js';
|
|
import { adminUsersRouter } from './routes/admin-users.js';
|
|
import { lessonsRouter } from './routes/lessons.js';
|
|
import { cardsRouter } from './routes/cards.js';
|
|
import { sessionsRouter } from './routes/sessions.js';
|
|
import { statsRouter } from './routes/stats.js';
|
|
import { subscriptionsRouter } from './routes/subscriptions.js';
|
|
import { marketplaceRouter } from './routes/marketplace.js';
|
|
import { adminLessonsRouter } from './routes/admin-lessons.js';
|
|
import { searchRouter } from './routes/search.js';
|
|
|
|
export function createApp(db: Db): Express {
|
|
const app = express();
|
|
app.set('trust proxy', 1);
|
|
app.use(cookieParser());
|
|
app.use(express.json({ limit: '5mb' }));
|
|
app.use(ensureCsrfToken);
|
|
app.use(currentUserOrNull(db));
|
|
|
|
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
|
|
|
// Public auth endpoints (logout/profile/change-password have their own requireAuth + verifyCsrf
|
|
// applied inside the router; public ones bootstrap cookies so CSRF cannot apply yet).
|
|
app.use('/api/auth', authRouter(db));
|
|
|
|
// Protected app routes
|
|
app.use('/api/lessons', requireAuth, verifyCsrf, lessonsRouter(db));
|
|
app.use('/api', requireAuth, verifyCsrf, cardsRouter(db));
|
|
app.use('/api/sessions', requireAuth, verifyCsrf, sessionsRouter(db));
|
|
app.use('/api/stats', requireAuth, verifyCsrf, statsRouter(db));
|
|
app.use('/api/admin/users', requireAuth, requireRole('sysadmin'), verifyCsrf, adminUsersRouter(db));
|
|
app.use('/api/admin/lessons', requireAuth, requireRole('sysadmin'), verifyCsrf, adminLessonsRouter(db));
|
|
app.use('/api', requireAuth, verifyCsrf, subscriptionsRouter(db));
|
|
app.use('/api/marketplace', requireAuth, marketplaceRouter(db));
|
|
app.use('/api/search', requireAuth, searchRouter(db));
|
|
|
|
// Static frontend in production
|
|
const frontendDist = resolve(import.meta.dirname, '../../frontend/dist');
|
|
if (existsSync(frontendDist)) {
|
|
app.use(express.static(frontendDist));
|
|
app.get('*', (_req, res) => res.sendFile(resolve(frontendDist, 'index.html')));
|
|
}
|
|
|
|
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
|
if (err instanceof ZodError) {
|
|
res.status(400).json({ error: { code: 'VALIDATION_ERROR', message: 'Invalid input', details: err.flatten() } });
|
|
return;
|
|
}
|
|
if (err instanceof ApiError) {
|
|
res.status(err.status).json({ error: { code: err.code, message: err.message, details: err.details } });
|
|
return;
|
|
}
|
|
console.error(err);
|
|
res.status(500).json({ error: { code: 'INTERNAL', message: 'Internal server error' } });
|
|
});
|
|
|
|
return app;
|
|
}
|