378 lines
12 KiB
Markdown
378 lines
12 KiB
Markdown
|
|
# 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
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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)
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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.
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
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.
|