feat(api): implement secure portfolio history, daily FX and atomic backups

This commit is contained in:
陈煜 committed 2026-10-01 16:21:16 +08:00
1 parent 67220e38ed
commit 99174a3da5
25 files changed
+1750 -28

No files matched your search

+67 -6
View File
@@ -1,8 +1,69 @@
import 'reflect-metadata';
import 'dotenv/config';
import {Module,Controller,Get} from '@nestjs/common';
import {NestFactory} from '@nestjs/core';
import {PrismaClient} from '@prisma/client';
@Controller('api') class HealthController {@Get('health') health(){return {status:'ok',app:'WorthPath'};}}
@Module({controllers:[HealthController]}) class AppModule {}
async function bootstrap(){ const db=new PrismaClient();await db.$connect(); const app=await NestFactory.create(AppModule);await app.listen(Number(process.env.PORT||3000),'0.0.0.0');}void bootstrap();
import { Module, Catch, ArgumentsHost, ExceptionFilter, HttpException } from '@nestjs/common';
import { NestFactory, APP_GUARD } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { json } from 'express';
import { AuthController, AuthGuard, AuthService } from './auth';
import { PortfolioController } from './portfolio';
import { BackupController } from './backup';
import { Database } from './database';
import { RatesService, SettingsController } from './rates';
import { ZodError } from 'zod';
import { Prisma } from '@prisma/client';
@Catch()
class SafeErrors implements ExceptionFilter {
catch(error: unknown, host: ArgumentsHost) {
let status = 500,
message = '服务暂时不可用,请稍后重试';
if (error instanceof ZodError) {
status = 400;
message = error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(';');
} else if (error instanceof HttpException) {
status = error.getStatus();
message = error.message;
} else if (
error instanceof Prisma.PrismaClientKnownRequestError &&
['P2002', 'P2034'].includes(error.code)
) {
status = 409;
message = '数据已存在或已被其他操作更新,请刷新后重试';
} else if (error instanceof SyntaxError) {
status = 400;
message = 'JSON 文件或请求格式无效';
} else if ((error as { status?: number })?.status === 413) {
status = 413;
message = '文件不能超过 8 MB';
}
host.switchToHttp().getResponse().setHeader('Cache-Control', 'no-store');
host.switchToHttp().getResponse().status(status).json({ message });
}
}
@Module({
providers: [Database, AuthService, RatesService, { provide: APP_GUARD, useClass: AuthGuard }],
controllers: [AuthController, PortfolioController, SettingsController, BackupController],
})
class AppModule {}
async function bootstrap() {
if (!process.env.DATABASE_URL || !process.env.WEB_ORIGIN)
throw Error('Missing local environment configuration');
if (process.env.NODE_ENV === 'production' && process.env.COOKIE_SECURE !== 'true')
throw Error('Production requires secure cookies');
const app = await NestFactory.create(AppModule, { logger: false, bodyParser: false });
app.use(helmet());
app.use(json({ limit: '8mb' }));
app.use(cookieParser());
app.use((_req: unknown, res: { setHeader: (k: string, v: string) => void }, next: () => void) => {
res.setHeader('Cache-Control', 'no-store');
next();
});
app.useGlobalFilters(new SafeErrors());
app.enableShutdownHooks();
await app.listen(Number(process.env.PORT || 3100), '0.0.0.0');
console.log('WorthPath API ready');
}
void bootstrap().catch(() => {
console.error('API startup failed. Check local configuration and database availability.');
process.exitCode = 1;
});