import { existsSync } from 'node:fs'; import Fastify from 'fastify'; import cookie from '@fastify/cookie'; import cors from '@fastify/cors'; import rateLimit from '@fastify/rate-limit'; import fastifyStatic from '@fastify/static'; import { ZodError } from 'zod'; import { db } from './shared/database/client'; import { env } from './shared/env'; import { formatZodError, isAppError } from './shared/errors'; import { AuthRepository } from './features/auth/auth.repository'; import { AuthService } from './features/auth/auth.service'; import { registerAuthRoutes } from './features/auth/auth.routes'; import { GameRepository } from './features/game/game.repository'; import { GameService } from './features/game/game.service'; import { registerGameRoutes } from './features/game/game.routes'; const app = Fastify({ logger: true }); await app.register(cookie, { secret: env.sessionSecret, }); await app.register(cors, { origin: env.webOrigin, credentials: true, }); await app.register(rateLimit, { max: env.isProduction ? 100 : 1000, timeWindow: '1 minute', }); const authRepository = new AuthRepository(db); const authService = new AuthService(authRepository); const gameRepository = new GameRepository(db); const gameService = new GameService(gameRepository); app.get('/api/health', async () => ({ ok: true, service: 'tinywaste-api', })); registerAuthRoutes(app, authService); registerGameRoutes(app, authService, gameService); const webDistAvailable = existsSync(env.webDistPath); if (webDistAvailable) { await app.register(fastifyStatic, { root: env.webDistPath, prefix: '/', maxAge: '30d', immutable: true, }); app.get('/', async (_request, reply) => reply.sendFile('index.html', { maxAge: 0, immutable: false, }), ); app.setNotFoundHandler((request, reply) => { const acceptsHtml = request.headers.accept?.includes('text/html'); if (request.method === 'GET' && acceptsHtml && !request.url.startsWith('/api/')) { return reply.sendFile('index.html', { maxAge: 0, immutable: false, }); } return reply.status(404).send({ error: { code: 'NOT_FOUND', message: '请求的资源不存在。', }, }); }); } app.setErrorHandler((error, _request, reply) => { if (isAppError(error)) { reply.status(error.statusCode).send({ error: { code: error.code, message: error.message, }, }); return; } if (error instanceof ZodError) { reply.status(400).send({ error: { code: 'VALIDATION_ERROR', message: formatZodError(error), }, }); return; } app.log.error(error); reply.status(500).send({ error: { code: 'INTERNAL_ERROR', message: '服务器内部错误,请稍后再试。', }, }); }); const start = async () => { try { await app.listen({ port: env.port, host: '0.0.0.0' }); app.log.info(`API running on http://localhost:${env.port}`); if (webDistAvailable) { app.log.info(`Web bundle served from ${env.webDistPath}`); } } catch (error) { app.log.error(error); process.exit(1); } }; await start();