┌─────────────────┐
│ HTTP Client │
│ (Frontend/API) │
└────────┬────────┘
│
│ 1. POST /mgnt/auth/login
│ Headers: x-request-id (optional)
│ Body: { username, password }
│
▼
┌─────────────────────────────────────────────────────┐
│ Fastify Server │
└─────────────────────────────────────────────────────┘
│
│ 2. Request enters NestJS pipeline
│
▼
┌─────────────────────────────────────────────────────┐
│ CorrelationInterceptor (NEW) │
│ • Extract/generate x-request-id │
│ • Attach to req.correlationId │
│ • Add to response headers │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ LoggingInterceptor │
│ • Log: "[uuid] +++ 请求:POST -> /mgnt/auth/login" │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Guards (Executed in Order) │
│ 1. RateLimitGuard (NEW) │
│ • Check IP + endpoint bucket │
│ • Increment counter │
│ • Throw 429 if over limit (10/min) │
│ │
│ 2. LocalAuthGuard │
│ • Validate credentials │
│ • Call AuthService.validateUser() │
│ • Attach user to request │
│ │
│ 3. MfaGuard (if applied) (NEW) │
│ • Check if user.twoFA enabled │
│ • Verify req.mfaVerified === true │
│ • Throw 401 if MFA required but not verified │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Controller Method │
│ @Post('login') │
│ async login(@AuthUser() user, @Req() req) { │
│ return this.authService.login(user, req) │
│ } │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Service Layer │
│ AuthService.login() │
│ • Create login log │
│ • Fetch roles & menus (parallel) │
│ • Generate JWT token │
│ • Update user.jwtToken in DB │
│ • Return: { account, token, avatar, ... } │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ OperationLogInterceptor │
│ • Capture method call (on success/error) │
│ • Log operation to sys_operation_log │
│ • Include req.body, response, correlation ID │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ ResponseInterceptor (UPDATED) │
│ Transform: │
│ { account, token, avatar, ... } │
│ Into: │
│ { │
│ success: true, │
│ code: "OK", │
│ message: "success", │
│ data: { account, token, avatar, ... }, │
│ timestamp: "2025-11-20T12:34:56.789Z" │
│ } │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ LoggingInterceptor │
│ • Log: "[uuid] --- 响应:POST -> /mgnt/auth/login │
│ +45ms" │
└─────────────────────────────────────────────────────┘
│
│ 3. HTTP Response
│ Status: 200 OK
│ Headers:
│ x-request-id: 550e8400-e29b...
│ x-correlation-id: 550e8400-e29b...
│ Body:
│ { success: true, code: "OK", ... }
│
▼
┌─────────────────┐
│ HTTP Client │
│ (Receives) │
└─────────────────┘
┌─────────────────┐
│ HTTP Client │
└────────┬────────┘
│
│ POST /mgnt/auth/login
│ { username: "test", password: "wrong" }
│
▼
... (same interceptors/guards) ...
│
▼
┌─────────────────────────────────────────────────────┐
│ AuthService.validateUser() │
│ • Compare password │
│ • ❌ Password mismatch │
│ • Throw: new BadRequestException({ │
│ statusCode: 400, │
│ message: '用户名或密码错误' │
│ }) │
└─────────────────────────────────────────────────────┘
│
│ Exception thrown
│
▼
┌─────────────────────────────────────────────────────┐
│ OperationLogInterceptor (error handler) │
│ • Catch exception │
│ • Call exceptionService.getHttpResponse(error) │
│ • Log to sys_operation_log with status: false │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ HttpExceptionFilter (NEW) │
│ • Catch BadRequestException │
│ • Extract status: 400 (was 200 before!) │
│ • Map to code: "BAD_REQUEST" │
│ • Extract message: "用户名或密码错误" │
│ • Build ApiResponse: │
│ { │
│ success: false, │
│ code: "BAD_REQUEST", │
│ message: "用户名或密码错误", │
│ data: null, │
│ timestamp: "2025-11-20T12:34:56.789Z" │
│ } │
│ • Log: logger.warn("[uuid] POST /mgnt/auth/login │
│ - 400 - 用户名或密码错误") │
│ • response.status(400).send(apiResponse) │
└─────────────────────────────────────────────────────┘
│
│ 4. HTTP Response
│ Status: 400 Bad Request (was 200!)
│ Headers: x-request-id, x-correlation-id
│ Body: { success: false, code: "BAD_REQUEST", ... }
│
▼
┌─────────────────┐
│ HTTP Client │
│ (Handles 400) │
└─────────────────┘
┌─────────────────┐
│ HTTP Client │
│ (IP: 1.2.3.4) │
└────────┬────────┘
│
│ Request #1-10: POST /mgnt/auth/login
│
▼
┌─────────────────────────────────────────────────────┐
│ RateLimitGuard │
│ │
│ buckets = Map { │
│ "1.2.3.4:POST:/mgnt/auth/login": { │
│ count: 10, │
│ resetAt: timestamp + 60000ms │
│ } │
│ } │
│ │
│ • Check bucket count │
│ • count <= 10 → ✅ ALLOW │
└─────────────────────────────────────────────────────┘
│
▼
... (continues to controller) ...
│ Request #11: POST /mgnt/auth/login
│
▼
┌─────────────────────────────────────────────────────┐
│ RateLimitGuard │
│ │
│ • Increment: count = 11 │
│ • count > 10 → ❌ BLOCK │
│ • Calculate retryAfter = (resetAt - now) / 1000 │
│ • Throw: new HttpException({ │
│ statusCode: 429, │
│ message: "Too many requests. Please try │
│ again in 45 seconds.", │
│ code: "RATE_LIMITED", │
│ retryAfter: 45 │
│ }, HttpStatus.TOO_MANY_REQUESTS) │
└─────────────────────────────────────────────────────┘
│
│ Exception thrown
│
▼
┌─────────────────────────────────────────────────────┐
│ HttpExceptionFilter │
│ • Status: 429 │
│ • Code: "RATE_LIMITED" │
│ • Message: "Too many requests..." │
└─────────────────────────────────────────────────────┘
│
│ HTTP 429 Response
│
▼
┌─────────────────┐
│ HTTP Client │
│ (Rate limited) │
└─────────────────┘
After 60 seconds...
│ Request #12: POST /mgnt/auth/login
│
▼
┌─────────────────────────────────────────────────────┐
│ RateLimitGuard │
│ │
│ • now > bucket.resetAt │
│ • Create new bucket: │
│ { │
│ count: 1, │
│ resetAt: now + 60000ms │
│ } │
│ • count <= 10 → ✅ ALLOW │
└─────────────────────────────────────────────────────┘
┌─────────────────┐
│ HTTP Client │
└────────┬────────┘
│
│ 1. POST /mgnt/auth/login
│ { username: "admin", password: "correct" }
│
▼
... (guards pass) ...
│
▼
┌─────────────────────────────────────────────────────┐
│ AuthService.login2fa() │
│ │
│ • Check: user.twoFA === "encrypted_secret" │
│ • twoFAEnabled = true │
│ • Check: req.mfaVerified === undefined │
│ • ❌ MFA not verified yet │
│ │
│ • Generate MFA stage token: │
│ payload = { │
│ userId, username, roleIds, │
│ stage: 'mfa' // ⚠️ Special flag │
│ } │
│ mfaToken = jwt.sign(payload, { expiresIn: '3m' })│
│ │
│ • Return: │
│ { │
│ twoFARequired: true, │
│ mfaToken, │
│ account, avatar, nick, roleInfo, menus │
│ } │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ HTTP Client │
│ • Store mfaToken│
│ • Show 2FA UI │
│ • User enters │
│ TOTP code │
└────────┬────────┘
│
│ 2. POST /mgnt/auth/2fa/verify
│ Headers: Authorization: Bearer <mfaToken>
│ Body: { code: "123456" }
│
▼
... (JWT guard validates mfaToken) ...
│
▼
┌─────────────────────────────────────────────────────┐
│ TwoFAService.verifyAtLogin() │
│ │
│ • Decrypt user.twoFA to get secret │
│ • Generate TOTP from secret │
│ • Compare with user input: "123456" │
│ • Check not recently used (prevent replay) │
│ • ✅ Code valid │
│ • Set: req.mfaVerified = true │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ AuthService.login() - Second Call │
│ │
│ • Check: req.mfaVerified === true ✅ │
│ • Generate final token: │
│ payload = { │
│ userId, username, roleIds, │
│ mfa: true // ✅ MFA verified │
│ } │
│ token = jwt.sign(payload, { expiresIn: '12h' }) │
│ │
│ • Update: user.jwtToken = token │
│ • Return: { account, token, avatar, ... } │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ HTTP Client │
│ • Store token │
│ • Navigate to │
│ dashboard │
└─────────────────┘
Later request to protected endpoint...
│ DELETE /mgnt/users/123
│ Headers: Authorization: Bearer <token>
│
▼
┌─────────────────────────────────────────────────────┐
│ JwtAuthGuard │
│ • Validate token │
│ • Extract payload: { userId, mfa: true } │
│ • Attach user to request │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ MfaGuard (NEW) │
│ • Check: user.twoFA exists → true │
│ • Check: payload.mfa === true → ✅ PASS │
│ • Allow request │
└─────────────────────────────────────────────────────┘
│
▼
... (continues to controller) ...
┌─────────────────┐
│ Application │
│ Startup │
└────────┬────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ ConfigModule.forRoot() │
│ • Load .env.mgnt.dev │
│ • Load .env │
│ • Merge with process.env │
│ • Call: validate(config) │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ validateEnvironment(config) │
│ │
│ • Transform to EnvironmentVariables class │
│ • Run class-validator decorators: │
│ - @IsUrl() MYSQL_URL │
│ - @IsUrl() MONGO_URL │
│ - @IsString() JWT_SECRET │
│ - @IsInt() @Min(60) JWT_EXPIRES_IN_SECONDS │
│ - etc. │
│ │
│ • Check validation results │
└─────────────────────────────────────────────────────┘
│
├─── ✅ All valid
│
│
▼
┌─────────────────────────────────────────────────────┐
│ Application Continues Startup │
│ • ConfigService populated with validated config │
│ • Type-safe access: configService.get('JWT_SECRET')│
└─────────────────────────────────────────────────────┘
│
▼
[App Running]
│
├─── ❌ Validation failed
│
▼
┌─────────────────────────────────────────────────────┐
│ Throw ValidationError │
│ │
│ Error: Environment validation failed: │
│ - JWT_SECRET should not be empty │
│ - MYSQL_URL must be a URL address │
│ - JWT_EXPIRES_IN_SECONDS must be at least 60 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Application Exits │
│ • Exit code: 1 │
│ • Error logged to console │
│ • ❌ App does NOT start with invalid config │
└─────────────────────────────────────────────────────┘
Request → Guards → Controller → Service →
ResponseInterceptor (wrap in {error, status, data}) →
AllExceptionsFilter (always HTTP 200) →
Response
Request →
CorrelationInterceptor (add UUID) →
LoggingInterceptor →
RateLimitGuard (check limit) →
Guards →
MfaGuard (if applied) →
Controller →
Service →
OperationLogInterceptor →
ResponseInterceptor (wrap in ApiResponse<T>) →
LoggingInterceptor →
HttpExceptionFilter (preserve status) →
Response (with correlation ID headers)
Request Identification
Early Rejection
Layered Security
Proper HTTP Semantics
Type Safety
ApiResponse<T> ensures type correctnessObservability