5c41eb410b
- 新增HUD图标、面板和状态指示器资产 - 实现HUD图标组件和错误边界组件 - 重构顶部状态栏和底部导航栏 - 更新路线面板样式和交互 - 添加HUD资产清单和切片脚本 - 移除未使用的资产文件 - 调整API安全配置和生产环境设置
127 lines
3.1 KiB
TypeScript
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();
|