Files
photo-live/services/api/src/app-cn.module.ts
T
浅念 a2a3ccd340 feat(api): idempotent upload session init with presigned credential
POST /v1/uploads (Task 18, A03): @@unique(userId, clientTaskId) idempotency
anchor — same params return the original session (201, no duplicate photo);
param drift (album/bytes/contentType/extension) -> 409 UPLOAD_SESSION_CONFLICT.
photos.objectKey is set to the FINAL originals key at init while the session
holds the staging key; IDs come from an app-side cuid-like generator so both
self-referencing keys are written in one transaction. Credential is a real
MinIO presigned PUT (SIGNED_URL_TTL_SECONDS, lazily read). Permission order:
album invisible 404 -> member 403 -> uploadEnabled 409 -> size/ext 400 ->
category ownership 400. No audit on init (audit anchors on complete).

Pre-step for Task 19: QUEUE_PHOTO_PROCESS contract constant + lazy-safe
BullMQ queue producer (Redis down at boot must not crash the app; enqueue
failure only logs — sweep compensates). e2e reset now also wipes the MinIO
photo-live/dev prefix; jest maps @photo/storage to TS source.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 22:42:13 +08:00

77 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// services/api/src/app-cn.module.ts
// 根模块:Prisma(@Global,封装 @photo/db-cn)、日志(nestjs-pino / pino-http)、
// request-id 中间件、全局异常过滤器、全局校验管道(Task 13 起,DTO 经
// class-validator 声明、ValidationPipe 强制,失败由过滤器映射 400 VALIDATION_FAILED)、
// /v1/health、/v1/auth/*(Task 13)。
// 功能模块(albums/uploads/photos/public)自 Task 15+ 起逐个加入。
// 过滤器/管道经 APP_FILTER/APP_PIPE、中间件经 NestModule 在模块层注册——e2e 直接
// import 本模块即可获得与生产一致的管线(全局前缀除外,由入口设置)。
import {
type MiddlewareConsumer,
Module,
type NestModule,
ValidationPipe,
} from '@nestjs/common';
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
import { LoggerModule } from 'nestjs-pino';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { AlbumsModule } from './albums/albums.module';
import { AllExceptionsFilter } from './all-exceptions.filter';
import { AuthModule } from './auth/auth.module';
import { HealthController } from './health.controller';
import { PrismaModule } from './prisma/prisma.module';
import { RequestIdMiddleware, resolveRequestId } from './request-id.middleware';
import { UploadsModule } from './uploads/uploads.module';
import { resolveLogLevel } from './shared/log-level';
import type { ServerRequestWithId } from './shared/request-with-id';
@Module({
imports: [
// @Global:功能模块(Task 12+)直接注入 PrismaService,无需 re-import。
PrismaModule,
AuthModule,
// 相册域(Task 14 起步:PermissionsService;控制器自 Task 15 加入)。
AlbumsModule,
// 上传域(Task 18 init;Task 19 renew/complete/history)。
UploadsModule,
LoggerModule.forRoot({
pinoHttp: {
// 与 RequestIdMiddleware 共用同一解析函数:优先透传 x-request-id,
// 其次复用已生成的 req.id,否则生成 UUID;并回写响应头。
genReqId: (req: IncomingMessage, res: ServerResponse) =>
resolveRequestId(req as ServerRequestWithId, res),
redact: {
// 授权头 + Cookie(会话凭据,Task 7 遗留项③):请求/响应两侧都打码,
// 防止日志聚合层留存可重放的凭据。
paths: [
'req.headers.authorization',
'req.headers.cookie',
'res.headers.authorization',
'res.headers.set-cookie',
],
censor: '[REDACTED]',
},
// Task 7 遗留项⑤:非法 LOG_LEVEL 在模块装配期(即启动期)快速失败,
// 而不是让 pino 首次写日志时才炸或静默吞掉输出。
level: resolveLogLevel(process.env.LOG_LEVEL),
},
}),
],
controllers: [HealthController],
providers: [
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
// 全局校验管道(Task 13):whitelist 剥离 DTO 未声明的属性(契约未禁未知
// 字段,剥离而非拒绝);校验失败抛 BadRequestException → 过滤器映射
// 400 VALIDATION_FAILED。经 APP_PIPE 在模块层注册,e2e 复用同一管线。
{
provide: APP_PIPE,
useFactory: () => new ValidationPipe({ whitelist: true }),
},
],
})
export class AppCnModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}