import 'express-async-errors'; /** * Express 应用主入口 */ import express, { type Request, type Response, } from 'express'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import cors from 'cors'; import dotenv from 'dotenv'; import notesRoutes from './routes/notes.js'; import imagesRoutes from './routes/images.js'; import annotationsRoutes from './routes/annotations.js'; import projectsRoutes from './routes/projects.js'; import commentsRoutes from './routes/comments.js'; import authRoutes from './routes/auth.js'; import { optionalAuth } from './auth.js'; import groupsRoutes from './routes/groups.js'; import managementRoutes from './routes/management.js'; import storageRoutes from './routes/storage.js'; import reviewRoutes from './routes/review.js'; import worksRoutes from './routes/works.js'; import projectWorksRoutes from './routes/projectWorks.js'; import { UPLOADS_DIR } from './upload.js'; import { database, databaseDialect } from './database.js'; dotenv.config(); const app: express.Application = express(); const __dirname = path.dirname(fileURLToPath(import.meta.url)); app.set('trust proxy', 1); const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((item) => item.trim()).filter(Boolean); app.use(cors({ origin: allowedOrigins?.length ? allowedOrigins : true, credentials: true })); app.use(express.json({ limit: '20mb' })); app.use(express.urlencoded({ extended: true, limit: '20mb' })); app.use(optionalAuth); // 静态图片资源 app.use( '/uploads', express.static(UPLOADS_DIR, { maxAge: '7d', immutable: true, }), ); /** * API 路由 */ app.use('/api/notes', notesRoutes); app.use('/api/works', worksRoutes); app.use('/api/projects/:projectId/works', projectWorksRoutes); app.use('/api/images', imagesRoutes); app.use('/api/annotations', annotationsRoutes); app.use('/api/projects', projectsRoutes); app.use('/api', commentsRoutes); app.use('/api/auth', authRoutes); app.use('/api/groups', groupsRoutes); app.use('/api/management', managementRoutes); app.use('/api/management/storage-configs', storageRoutes); app.use('/api/review', reviewRoutes); /** * health */ app.use('/api/health', async (_req: Request, res: Response) => { await database.one('SELECT 1 AS ready'); res.status(200).json({ success: true, message: 'ok', database: databaseDialect }); }); const distDir = path.resolve(__dirname, '..', 'dist'); if (process.env.NODE_ENV === 'production' && fs.existsSync(distDir)) { app.use(express.static(distDir, { maxAge: '1h' })); app.get('*', (req, res, next) => { if (req.path.startsWith('/api/')) { next(); return; } res.sendFile(path.join(distDir, 'index.html')); }); } /** * 错误处理 */ app.use((err: Error, _req: Request, res: Response, _next: unknown) => { void _next; // multer 文件类型/大小错误 const message = err.message || '服务器内部错误'; const errorCode = (err as Error & { code?: string }).code; const statusCode = Number((err as Error & { statusCode?: number }).statusCode); if (errorCode === 'LIMIT_FILE_SIZE') { res.status(413).json({ success: false, error: '单张图片不能超过 20 MB' }); return; } if (errorCode === 'LIMIT_FILE_COUNT' || errorCode === 'LIMIT_UNEXPECTED_FILE') { res.status(400).json({ success: false, error: '每次最多上传 30 张图片' }); return; } if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599) { res.status(statusCode).json({ success: false, error: message }); return; } if (err.message?.includes('不支持的文件类型')) { res.status(400).json({ success: false, error: message }); return; } console.error('[API Error]', err); res.status(500).json({ success: false, error: message }); }); /** * 404 */ app.use((_req: Request, res: Response) => { res.status(404).json({ success: false, error: 'API not found' }); }); export default app;