# Request Lifecycle — Login then Dashboard

What actually happens when a user signs in and opens the dashboard. Use this when debugging auth or correlation-id flow.

## Login

```mermaid
sequenceDiagram
    participant Browser
    participant Nginx as nginx / Ingress
    participant API as .NET API
    participant DB as Postgres
    participant Redis

    Browser->>Nginx: POST /api/auth/login<br/>{ email, password }<br/>X-Correlation-Id: cid-A
    Nginx->>Nginx: rate-limit auth_zone (10r/m)
    Nginx->>API: forward (sets X-Forwarded-*)
    API->>API: CorrelationIdMiddleware<br/>(reuse cid-A, push to LogContext)
    API->>API: ExceptionMiddleware (wrap try)
    API->>API: AuthN — JWT not present here (login is anonymous)
    API->>API: MediatR LoggingBehavior — log "Handling LoginCommand"
    API->>API: ValidationBehavior — FluentValidation
    API->>DB: SELECT users JOIN user_roles JOIN role_permissions<br/>WHERE email=? AND deleted_at IS NULL
    DB-->>API: user row + roles + perms
    API->>API: Argon2PasswordHasher.Verify
    API->>API: JwtService.Issue (access, 15 min)
    API->>API: RefreshTokenService.Issue (new SHA-256 hash)
    API->>DB: INSERT refresh_tokens
    DB-->>API: ok
    API->>API: UPDATE users SET last_login_at = now()
    API->>API: ApiResponse.Ok({ tokens, user })
    Note over API,Redis: (queued audit emit lands when the<br/>.NET-side AuditDispatcher is wired — Phase 8+)
    API-->>Nginx: 200 ApiResponse<LoginResponse><br/>+ X-Correlation-Id: cid-A
    Nginx-->>Browser: 200 (cid-A echoed)
    Browser->>Browser: AuthService.persist tokens<br/>(localStorage + signal)
    Browser->>Browser: router.navigate('/dashboard')
```

## Dashboard load

```mermaid
sequenceDiagram
    participant Browser
    participant Nginx
    participant API as .NET API
    participant DB

    Browser->>Browser: authGuard checks AuthService.isAuthenticated()
    Browser->>Browser: i18n.init() resolves Transloco lang
    Browser->>Nginx: GET /api/dashboard/summary<br/>Authorization: Bearer <token><br/>X-Correlation-Id: cid-B
    Nginx->>Nginx: rate-limit api_zone (100r/m)
    Nginx->>API: forward
    API->>API: CorrelationIdMiddleware (push cid-B)
    API->>API: JWT auth → ClaimsPrincipal
    API->>API: HttpCurrentUserContext exposes userId + tenantId + roles + perms
    API->>API: MediatR pipeline (Logging + Validation)
    API->>DB: AsNoTracking().Select(...) projection<br/>(counts via FILTER WHERE, tenant-scoped)
    DB-->>API: aggregated DTO (one round-trip)
    API->>API: ApiResponse.Ok(dto)
    API-->>Browser: 200 (cid-B echoed)
    Browser->>Browser: TransformInterceptor unwraps data field
    Browser->>Browser: DashboardComponent renders<br/>(signals → OnPush change-detect)
```

## Token refresh (before expiry)

```mermaid
sequenceDiagram
    participant Browser
    participant API as .NET API
    participant DB

    Browser->>API: POST /api/auth/refresh<br/>{ refreshToken: <plain> }<br/>X-Correlation-Id: cid-C
    API->>API: RefreshTokenService.Validate<br/>(SHA-256 hash lookup, expires_at > now, revoked_at IS NULL)
    API->>DB: SELECT refresh_tokens WHERE token_hash=?
    DB-->>API: existing row
    API->>API: JwtService.Issue new access token
    API->>API: RefreshTokenService.Issue new refresh token
    API->>DB: INSERT new + UPDATE old (revoked_at, replaced_by_token_hash)
    DB-->>API: ok
    API-->>Browser: 200 { tokens: { new access + new refresh } }
    Browser->>Browser: AuthService.persist replaces both
```

## Audit emit (Node-side producer, worker consumer)

```mermaid
sequenceDiagram
    participant API as api-nest (producer)
    participant Redis as BullMQ queue
    participant Worker as worker-nest
    participant DB as Postgres

    API->>API: Service emits AuditDispatcher.emit(event)
    API->>Redis: queue.add('audit-events', event)<br/>attempts=5, exp backoff
    Note over API: emit failures are logged + swallowed<br/>(audit must not break the caller)
    Redis-->>Worker: Worker pulls job
    Worker->>Worker: PrismaAuditSink.persist(event)
    Worker->>DB: INSERT audit_logs
    DB-->>Worker: ok
    Worker->>Redis: ack + removeOnComplete (3600s)
```

## Notification dispatch (queued)

```mermaid
sequenceDiagram
    participant Caller as any API host
    participant Redis as BullMQ
    participant Worker as worker-nest
    participant SMTP

    Caller->>Caller: NotificationDispatcher.send({ kind:'email', recipient, body })
    Caller->>Redis: queue.add('notifications', payload)<br/>attempts=3
    Redis-->>Worker: job
    Worker->>Worker: route by payload.kind → SmtpChannel
    Worker->>SMTP: nodemailer.sendMail
    SMTP-->>Worker: messageId
    Worker->>Redis: ack
```

## Correlation-id flow

Every request carries `X-Correlation-Id`. It's:

- Generated by nginx or the first hop if missing
- Read by middleware, pushed into Serilog `LogContext` / pino bindings
- Echoed back in the response header (CORS allows it)
- Persisted on every `audit_logs` row + `refresh_tokens.user_agent` (where relevant)
- Threaded into BullMQ job metadata so the worker logs share the same id

This lets you trace one user action end-to-end across processes:

```bash
# In Loki
{job="api-dotnet"} |= "cid-A"
{job="worker-nest"} |= "cid-A"
```

## Related

- [`overview.md`](overview.md) — C4 system + container + component diagrams
- `.claude/skills/audit-log-instrumentation/SKILL.md`
- `enterprise-modules/em-auth/README.md` — claim mapping
