Files
TinyWaste/apps/api/src/server.ts
T
virtheart 5c41eb410b feat(HUD): 添加HUD界面资产与组件
- 新增HUD图标、面板和状态指示器资产
- 实现HUD图标组件和错误边界组件
- 重构顶部状态栏和底部导航栏
- 更新路线面板样式和交互
- 添加HUD资产清单和切片脚本
- 移除未使用的资产文件
- 调整API安全配置和生产环境设置
2026-04-29 01:24:37 +08:00

127 lines
3.1 KiB
TypeScript

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();