Files
backend/docs/flows.md
Sathish c9b38dc6ab fix(security): resolve audit findings — secrets, env contract, migrations
Removes hardcoded fallback secrets and makes a misconfigured deploy fail
loudly instead of silently falling back to development defaults.

- Remove insecure JWT fallback secrets (messages.module, configuration)
- Remove the 'default-secret' fallback for the 2FA TOTP encryption key and
  allow a dedicated TWO_FACTOR_ENCRYPTION_KEY so rotating JWT_SECRET no
  longer locks out every 2FA user (see docs/2fa-key-rotation.md)
- Require EMAIL_API_URL; drop the hardcoded vendor email endpoint
- Drive WebSocket CORS from CORS_ORIGINS instead of origin:'*'
- Load .env before any Nest module is imported (src/load-env.ts). Decorator
  arguments evaluate at import time, so the gateway previously froze its CORS
  config to the localhost fallback even when CORS_ORIGINS was set
- Add boot-time env validation: missing required vars, weak JWT_SECRET, and
  inverted access/refresh token lifetimes now abort startup
- Enable Redis TLS certificate verification
- Require ADMIN_EMAIL/ADMIN_PASSWORD for the seed; remove the published
  default super-admin credentials and stop printing them
- Add the initial Prisma migration and stop gitignoring prisma/migrations
- Make .env.example an accurate configuration contract (admin bootstrap,
  REDIS_TLS, S3_ENDPOINT, 2FA key, Firebase path; drop the dead SMTP block)
- Add handover documentation: architecture, ER model, sequence and data-flow
  diagrams, 2FA key rotation runbook

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:03:08 +05:30

12 KiB

Workflow (sequence) diagrams

Core flows, traced against the delivered source. Mermaid sequence diagrams — editable and diffable in version control.

Endpoints are relative to the API prefix /api/v1.


1. Registration & email verification

sequenceDiagram
    participant C as Client
    participant API as AuthController
    participant S as AuthService
    participant DB as PostgreSQL
    participant EV as EventEmitter
    participant EL as EmailListener
    participant M as Email gateway

    C->>API: POST /auth/register
    API->>S: register(dto)
    S->>DB: find user by email
    alt email already exists
        S-->>C: 409 Conflict (states existing role)
    else new user
        S->>S: argon2.hash(password)
        S->>DB: create User (+ profile)
        S->>DB: create verification token
        S->>EV: emit "user.registered"
        S-->>C: 201 Created
    end
    EV-->>EL: user.registered
    EL->>M: POST EMAIL_API_URL (verification email)

    C->>API: POST /auth/verify-email { token }
    API->>DB: mark emailVerified
    API->>EV: emit "user.email-verified"
    EV-->>EL: user.email-verified
    EL->>M: POST EMAIL_API_URL (welcome email, role-aware)

Email delivery is asynchronous and best-effort: a failure is logged by EmailService and does not fail the registration request.


2. Login (with 2FA branch)

sequenceDiagram
    participant C as Client
    participant API as AuthController
    participant S as AuthService
    participant T as TwoFactorService
    participant DB as PostgreSQL

    C->>API: POST /auth/login { email, password }
    API->>DB: load user
    API->>S: argon2.verify
    alt invalid credentials
        S-->>C: 401 Unauthorized
    else 2FA enabled
        S->>T: generateTempToken(userId)
        S-->>C: 200 { requiresTwoFactor: true, tempToken }
        C->>API: POST /auth/2fa/verify { tempToken, code }
        API->>T: verifyTempToken(tempToken)
        T->>DB: read twoFactorSecret (encrypted)
        T->>T: AES-256-GCM decrypt, speakeasy.verify(code)
        alt code invalid
            T-->>C: 401 Unauthorized
        else code valid
            T->>DB: create Session (refresh token)
            T-->>C: 200 { accessToken, refreshToken }
        end
    else 2FA disabled
        S->>DB: create Session (refresh token)
        S-->>C: 200 { accessToken, refreshToken }
    end

Backup codes follow the same path via POST /auth/2fa/verify-backup.

Token refresh

sequenceDiagram
    participant C as Client
    participant API as AuthController
    participant DB as PostgreSQL

    C->>API: POST /auth/refresh { refreshToken }
    API->>DB: look up Session
    alt session missing / expired / revoked
        API-->>C: 401 — client must log in again
    else valid
        API->>DB: rotate Session
        API-->>C: { accessToken, refreshToken }
    end

JWT_REFRESH_EXPIRATION must be longer than JWT_ACCESS_EXPIRATION; if it is shorter, refresh fails before the access token it is meant to renew expires and users are logged out. Boot validation rejects this configuration.


3. Google sign-in

sequenceDiagram
    participant B as Browser
    participant NA as NextAuth (frontend)
    participant G as Google OAuth
    participant API as AuthController
    participant DB as PostgreSQL

    B->>NA: click "Continue with Google"
    NA->>G: OAuth authorisation code flow
    G-->>NA: id_token + profile
    NA->>NA: verify token, extract profile
    NA->>API: POST /auth/social { provider, email, name, providerId }
    API->>DB: find or create User (authProvider = GOOGLE)
    API->>DB: create Session
    API-->>NA: { accessToken, refreshToken }
    NA-->>B: session established

The OAuth client ID/secret live in the frontend environment. The GOOGLE_* variables in the backend environment are not consumed. Facebook and Twitter are not enabled.


4. Connection requests

sequenceDiagram
    participant U as User
    participant API as ConnectionRequestsController
    participant DB as PostgreSQL
    participant EV as EventEmitter
    participant N as NotificationsService
    participant A as Agent

    U->>API: POST /connection-requests { agentProfileId, message }
    API->>DB: create ConnectionRequest (status PENDING)
    API->>EV: emit "notification.connection_request"
    EV-->>N: handle
    N->>DB: create Notification
    N->>A: FCM push + email (per user preferences)

    A->>API: PATCH /connection-requests/:id/respond { status }
    API->>DB: update status (ACCEPTED / REJECTED)
    API->>EV: emit "notification.connection_response"
    EV-->>N: handle
    N->>U: notification + push
    note over DB: on ACCEPTED a Conversation becomes available

Supporting reads: GET /connection-requests/my-requests, /received, /received/counts, /status/:agentProfileId.


5. Real-time messaging

sequenceDiagram
    participant C1 as Sender
    participant GW as MessagesGateway
    participant SVC as MessagesService
    participant DB as PostgreSQL
    participant RD as Redis
    participant C2 as Recipient

    C1->>GW: connect (handshake.auth.token = JWT)
    GW->>GW: jwtService.verifyAsync
    alt invalid token
        GW-->>C1: disconnect
    else valid
        GW->>RD: mark user online (presence)
        GW->>GW: join room user:{userId}
        GW-->>C1: connected
    end

    C1->>GW: join_conversation { conversationId }
    GW->>GW: join room conversation:{id}

    C1->>GW: send_message { conversationId, content }
    GW->>SVC: create message
    SVC->>DB: insert Message
    SVC->>GW: emit "notification.message" (offline delivery)
    GW->>RD: publish via Socket.IO Redis adapter
    RD-->>GW: fan-out to other API instances
    GW-->>C2: new_message

    C2->>GW: mark_read { conversationId }
    GW->>DB: update read state
    GW-->>C1: messages_read

Other events: typing_start / typing_stop, message_received, user_status_change (presence broadcast), and the parallel support-chat set (support_join, support_send_message, support_new_message, …).

Ping interval and timeout are both 5s, so a dropped connection is detected in roughly 10s rather than the Socket.IO default of ~45s.

CORS on this gateway is driven by CORS_ORIGINS, read at module-import time — see src/load-env.ts.


6. Subscription, payment & Stripe webhooks

sequenceDiagram
    participant A as Agent
    participant API as StripeController
    participant ST as Stripe
    participant WH as StripeWebhookController
    participant DB as PostgreSQL

    A->>API: GET /stripe/plans
    API-->>A: SubscriptionPlan list
    A->>API: POST /stripe/create-checkout-session { planId }
    API->>ST: checkout.sessions.create
    ST-->>A: redirect to Stripe Checkout
    A->>ST: completes payment

    ST->>WH: POST /stripe/webhook (signed)
    WH->>WH: constructEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET)
    alt signature invalid
        WH-->>ST: 400
    else valid
        alt checkout.session.completed
            WH->>DB: create/activate AgentSubscription
        else invoice.paid
            WH->>DB: record Payment, extend period
        else invoice.payment_failed
            WH->>DB: mark PAST_DUE
        else customer.subscription.updated
            WH->>DB: sync status / plan
        else customer.subscription.deleted
            WH->>DB: mark CANCELED
        end
        WH-->>ST: 200
    end

The webhook route needs the raw request body for signature verification (rawBody: true is set on the Nest application). Any proxy that rewrites the body will break signature checks.

Self-service billing management goes through POST /stripe/create-portal-session; cancellation through POST /stripe/cancel-subscription.


7. Agent verification

sequenceDiagram
    participant AG as Agent
    participant API as UsersController
    participant DB as PostgreSQL
    participant AD as Admin
    participant EV as EventEmitter
    participant N as NotificationsService

    AG->>API: POST /users/me/verification/submit
    API->>DB: AgentProfile.verificationStatus = PENDING_REVIEW
    AD->>API: GET /users?verificationStatus=PENDING_REVIEW
    AD->>API: PATCH /users/:id/verification { status }
    API->>DB: update status + insert VerificationHistory (admin, reason)
    API->>EV: emit "notification.verification"
    EV-->>N: handle
    N->>AG: notification + push + email

VerificationHistory is the audit trail: who changed the status, when, and why. Note that approval is not gated on an active subscription in the backend — see architecture.md §4.


8. File upload

sequenceDiagram
    participant C as Client
    participant API as UploadController
    participant S3 as Object storage
    participant DB as PostgreSQL

    C->>API: POST /upload/presigned-url { filename, contentType }
    API->>API: validate type/size, build key with S3_FOLDER_PREFIX
    API->>S3: sign PUT URL
    API-->>C: { uploadUrl, key }
    C->>S3: PUT file (direct, bypasses the API)
    C->>API: PATCH resource with { key }
    API->>DB: store the object KEY (never a full URL)

    note over C,S3: reading back
    C->>API: GET /upload/presigned-download-url?key=...
    API-->>C: time-limited URL

Variants: message-presigned-url, avatar-presigned-url, user-avatar-presigned-url; deletion via DELETE /upload/:key.

Because only object keys are stored, any storage migration must preserve the key layout exactly or every existing reference breaks.


9. Transactional email — trigger events

All email is sent by POSTing JSON to EMAIL_API_URL. There is no SMTP path in the code; MAIL_* / SMTP_* variables are ignored.

Event Listener Email sent
user.registered EmailListener Verification email
user.email-verified EmailListener Welcome email (USER vs Professional/AGENT variant)
password.reset-requested EmailListener Password reset link
password.reset-completed EmailListener Confirmation
user.email-change-requested EmailListener Verification for the new address
notification fan-out NotificationsService Generic notification email (connection requests/responses, new messages, verification outcome) subject to user preferences

Templates live in backend/src/email/templates.


10. Data-flow diagram

Where data lives and who it crosses.

flowchart LR
    subgraph Clients
        W[Web]
        AD[Admin]
        MO[Mobile]
    end

    W -- "credentials, profile, messages" --> API
    AD -- "moderation actions" --> API
    MO -- "credentials, profile, messages" --> API

    API[Backend API]

    API -- "users, profiles, messages,<br/>subscriptions, audit log" --> PG[(PostgreSQL)]
    API -- "presence, socket rooms" --> RD[(Redis)]
    API -- "object keys only" --> PG
    W -- "file bytes (presigned PUT)" --> S3[(Object storage)]
    MO -- "file bytes (presigned PUT)" --> S3
    API -- "sign / delete" --> S3

    API -- "email address, name, links" --> MAIL[Email gateway]
    API -- "device token, title, body" --> FCM[Firebase FCM]
    API -- "customer id, price id, amount" --> STRIPE[Stripe]
    STRIPE -- "subscription + invoice events" --> API
    W -- "OAuth profile" --> GOOGLE[Google OAuth]

    classDef pii fill:#fde,stroke:#b47
    class MAIL,STRIPE,GOOGLE,FCM pii

Personal data leaving the platform boundary (highlighted): email address and display name to the email gateway; email and billing identifiers to Stripe; device tokens and notification content to Firebase; email and profile to Google during sign-in. Message bodies and uploaded files never leave PostgreSQL and object storage.