ARCHITECTURE_FLOW.md 26 KB

Request/Response Flow Architecture

New Request Flow (After Refactor)

┌─────────────────┐
│   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)   │
└─────────────────┘

Error Flow (After Refactor)

┌─────────────────┐
│   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)  │
└─────────────────┘

Rate Limit Flow (New)

┌─────────────────┐
│   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                           │
└─────────────────────────────────────────────────────┘

MFA Flow (New)

┌─────────────────┐
│   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) ...

Configuration Validation Flow (New)

┌─────────────────┐
│  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        │
└─────────────────────────────────────────────────────┘

Comparison: Before vs After

Before (Old Flow)

Request → Guards → Controller → Service →
  ResponseInterceptor (wrap in {error, status, data}) →
  AllExceptionsFilter (always HTTP 200) →
  Response

After (New Flow)

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)

Key Architectural Changes

  1. Request Identification

    • Every request has a unique correlation ID
    • Logged at every stage for traceability
  2. Early Rejection

    • Rate limiting happens before authentication
    • Saves resources on brute force attempts
  3. Layered Security

    • Multiple guards can be composed
    • Each guard has single responsibility
  4. Proper HTTP Semantics

    • Status codes match HTTP standards
    • Clients can handle errors properly
    • Monitoring tools work correctly
  5. Type Safety

    • Generic ApiResponse<T> ensures type correctness
    • Configuration validated at compile time and runtime
  6. Observability

    • Correlation IDs enable request tracing
    • Structured logging with context
    • Operation logs capture full request/response