2026-05-04 16:09:01 +08:00
|
|
|
import { NestFactory } from '@nestjs/core';
|
|
|
|
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
|
|
|
|
import { AppModule } from './app.module';
|
2026-05-09 18:57:33 +08:00
|
|
|
import helmet from 'helmet';
|
|
|
|
|
import { ConfigService } from '@nestjs/config';
|
|
|
|
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
2026-05-04 16:09:01 +08:00
|
|
|
|
|
|
|
|
async function bootstrap() {
|
2026-05-09 18:57:33 +08:00
|
|
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
|
|
|
|
const configService = app.get(ConfigService);
|
|
|
|
|
const isProduction = configService.get('app.nodeEnv') === 'production';
|
|
|
|
|
|
|
|
|
|
app.use(helmet());
|
2026-05-04 16:09:01 +08:00
|
|
|
|
2026-05-21 15:05:31 +08:00
|
|
|
app.setGlobalPrefix('api', { exclude: ['health', 'admin-api/(.*)', 'internal/(.*)'] });
|
2026-05-18 15:29:36 +08:00
|
|
|
|
2026-05-04 16:09:01 +08:00
|
|
|
app.enableCors({
|
2026-05-09 18:57:33 +08:00
|
|
|
origin: isProduction
|
|
|
|
|
? [configService.get('app.allowedOrigin', 'https://longde.cloud')]
|
|
|
|
|
: ['https://longde.cloud', 'http://localhost:4321', 'http://localhost:5173'],
|
2026-05-04 16:09:01 +08:00
|
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
|
|
|
credentials: true,
|
2026-05-09 18:57:33 +08:00
|
|
|
maxAge: 86400,
|
2026-05-04 16:09:01 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-09 18:57:33 +08:00
|
|
|
app.useBodyParser('json', { limit: '10mb' });
|
2026-05-04 16:09:01 +08:00
|
|
|
|
2026-05-09 18:57:33 +08:00
|
|
|
const swaggerEnabled = !isProduction || configService.get('app.enableSwagger') === true;
|
|
|
|
|
if (swaggerEnabled) {
|
|
|
|
|
const config = new DocumentBuilder()
|
|
|
|
|
.setTitle('知习 API')
|
|
|
|
|
.setDescription('知习 AI-first 系统化学习产品后端 API')
|
|
|
|
|
.setVersion('0.1.0')
|
|
|
|
|
.addBearerAuth()
|
|
|
|
|
.addTag('health', '服务健康检查')
|
|
|
|
|
.addTag('auth', '用户认证')
|
2026-05-21 15:05:31 +08:00
|
|
|
.addTag('admin-auth', '管理员认证')
|
2026-05-09 18:57:33 +08:00
|
|
|
.addTag('users', '用户管理')
|
|
|
|
|
.addTag('knowledge-base', '知识库')
|
|
|
|
|
.addTag('knowledge-items', '知识点')
|
|
|
|
|
.addTag('document-import', '资料导入')
|
|
|
|
|
.addTag('learning-session', '学习会话')
|
|
|
|
|
.addTag('active-recall', '主动回忆')
|
|
|
|
|
.addTag('ai-analysis', 'AI 分析')
|
|
|
|
|
.addTag('review', '复习管理')
|
|
|
|
|
.addTag('focus-items', '待巩固项')
|
|
|
|
|
.addTag('learning-activity', '学习活跃')
|
|
|
|
|
.addTag('notifications', '消息通知')
|
|
|
|
|
.addTag('feedback', '用户反馈')
|
2026-05-17 22:30:14 +08:00
|
|
|
.addTag('files', '文件管理')
|
2026-05-09 18:57:33 +08:00
|
|
|
.addTag('waitlist', '等待名单')
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
const document = SwaggerModule.createDocument(app, config);
|
|
|
|
|
|
|
|
|
|
if (isProduction) {
|
|
|
|
|
const swaggerUser = configService.get('app.swaggerUser', 'admin');
|
|
|
|
|
const swaggerPassword = configService.get('app.swaggerPassword');
|
|
|
|
|
if (swaggerPassword) {
|
2026-05-17 00:39:46 +08:00
|
|
|
const basicAuthMiddleware = (req: any, res: any, next: any) => {
|
2026-05-09 18:57:33 +08:00
|
|
|
const auth = req.headers.authorization;
|
|
|
|
|
if (!auth?.startsWith('Basic ')) {
|
|
|
|
|
res.setHeader('WWW-Authenticate', 'Basic');
|
|
|
|
|
return res.status(401).send('Authentication required');
|
|
|
|
|
}
|
|
|
|
|
const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64')
|
|
|
|
|
.toString()
|
|
|
|
|
.split(':');
|
|
|
|
|
if (user === swaggerUser && pass === swaggerPassword) {
|
|
|
|
|
return next();
|
|
|
|
|
}
|
|
|
|
|
return res.status(401).send('Invalid credentials');
|
2026-05-17 00:39:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
app.use('/api-docs', basicAuthMiddleware);
|
|
|
|
|
app.use('/api-docs-json', basicAuthMiddleware);
|
2026-05-09 18:57:33 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SwaggerModule.setup('api-docs', app, document, {
|
|
|
|
|
swaggerOptions: { persistAuthorization: true },
|
|
|
|
|
customCss: '.swagger-ui .topbar { display: none }',
|
|
|
|
|
customSiteTitle: '知习 API 文档',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.getHttpAdapter().get('/api-docs-json', (_req: any, res: any) => {
|
|
|
|
|
res.json(document);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('[Swagger] API 文档已启用');
|
|
|
|
|
}
|
2026-05-04 16:09:01 +08:00
|
|
|
|
feat: P2 infrastructure — Docker Compose, shutdown hooks, Prisma migration
- B20: docker-compose.yml with MySQL 8.0, Redis 7, API, BullMQ Worker, Nginx
- B20: Dockerfile.worker + worker.module.ts + worker.main.ts for standalone worker
- B20: nginx/nginx.conf reverse proxy with gzip, /api/* routes, health check
- B21: app.enableShutdownHooks() in main.ts for graceful SIGTERM handling
- B22: migration adding objectKey/bucket to UploadedFile, AiUsageLog, WaitlistEntry
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:50:59 +08:00
|
|
|
app.enableShutdownHooks();
|
|
|
|
|
|
2026-05-09 18:57:33 +08:00
|
|
|
const port = configService.get<number>('app.port', 3000);
|
2026-05-04 16:09:01 +08:00
|
|
|
await app.listen(port);
|
|
|
|
|
console.log(`[API] Server running on http://localhost:${port}`);
|
|
|
|
|
}
|
2026-05-09 18:25:04 +08:00
|
|
|
bootstrap();
|