143 lines
5.3 KiB
TypeScript
143 lines
5.3 KiB
TypeScript
import { networkConfig, isNetworkHostAllowed } from './network';
|
||
import 'reflect-metadata';
|
||
import 'dotenv/config';
|
||
import { setupOpenApi } from './openapi';
|
||
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, AuthBusinessService, AuthGuard, AuthService } from './auth';
|
||
import { AdminController, AdminService } from './admin';
|
||
import { CalendarController, CalendarBusinessService } from './calendar';
|
||
import { SchedulesController, SchedulesBusinessService } from './schedules';
|
||
import { TransfersController, TransfersBusinessService } from './transfers';
|
||
import { PortfolioController, PortfolioBusinessService } from './portfolio';
|
||
import { BackupController, BackupBusinessService } from './backup';
|
||
import { IconsController, IconsBusinessService, IconsService } from './icons';
|
||
import { MetalsService, MetalsController, MetalsBusinessService } from './metals';
|
||
import { Database } from './database';
|
||
import { RatesService, SettingsController, SettingsBusinessService } from './rates';
|
||
import { ZodError } from 'zod';
|
||
import { Prisma } from '@prisma/client';
|
||
import { AgentOAuth } from './mcp/oauth';
|
||
import { AgentCatalogue } from './mcp/catalogue';
|
||
import { AgentOperations } from './mcp/operations';
|
||
import { AgentFiles } from './mcp/files';
|
||
import { AgentTransport } from './mcp/transport';
|
||
import { AgentManagementController } from './mcp/management';
|
||
@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,
|
||
AdminService,
|
||
RatesService,
|
||
MetalsService,
|
||
IconsService,
|
||
AuthBusinessService,
|
||
CalendarBusinessService,
|
||
SchedulesBusinessService,
|
||
TransfersBusinessService,
|
||
PortfolioBusinessService,
|
||
BackupBusinessService,
|
||
IconsBusinessService,
|
||
MetalsBusinessService,
|
||
SettingsBusinessService,
|
||
AgentOAuth,
|
||
AgentCatalogue,
|
||
AgentOperations,
|
||
AgentFiles,
|
||
AgentTransport,
|
||
{ provide: APP_GUARD, useClass: AuthGuard },
|
||
],
|
||
controllers: [
|
||
TransfersController,
|
||
SchedulesController,
|
||
CalendarController,
|
||
IconsController,
|
||
AuthController,
|
||
AdminController,
|
||
PortfolioController,
|
||
MetalsController,
|
||
SettingsController,
|
||
BackupController,
|
||
AgentManagementController,
|
||
],
|
||
})
|
||
class AppModule {}
|
||
async function bootstrap() {
|
||
if (!process.env.DATABASE_URL || !process.env.WEB_ORIGIN)
|
||
throw Error('Missing local environment configuration');
|
||
const network = networkConfig();
|
||
if (
|
||
!network.allowWildcardOrigins &&
|
||
process.env.WEB_ORIGIN.split(',').some((v) => v.trim() === '*')
|
||
)
|
||
throw Error('Production requires an explicit web origin');
|
||
if (network.requireSecureCookie && !network.cookieSecure)
|
||
throw Error('Production requires secure cookies');
|
||
const app = await NestFactory.create(AppModule, { logger: false, bodyParser: false });
|
||
app.use(
|
||
helmet({
|
||
strictTransportSecurity: network.hsts ? undefined : false,
|
||
contentSecurityPolicy: {
|
||
directives: { 'upgrade-insecure-requests': network.upgradeInsecureRequests ? [] : null },
|
||
},
|
||
}),
|
||
);
|
||
app.use((req: { headers: { host?: string } }, res: any, next: () => void) => {
|
||
if (!isNetworkHostAllowed(req.headers.host, network.apiAllowedHosts))
|
||
return res.status(403).json({ message: '请求 Host 不受信任' });
|
||
next();
|
||
});
|
||
const origins = process.env.WEB_ORIGIN.split(',').map((v) => v.trim());
|
||
app.enableCors({
|
||
origin: network.allowWildcardOrigins && origins.includes('*') ? true : origins,
|
||
credentials: true,
|
||
});
|
||
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.get(AgentTransport).install(app.getHttpAdapter().getInstance());
|
||
app.useGlobalFilters(new SafeErrors());
|
||
setupOpenApi(app);
|
||
app.enableShutdownHooks();
|
||
await app.listen(Number(process.env.PORT || 3100), network.apiHost);
|
||
console.log('WorthPath API ready');
|
||
}
|
||
void bootstrap().catch(() => {
|
||
console.error('API startup failed. Check local configuration and database availability.');
|
||
process.exitCode = 1;
|
||
});
|