Files
nick-doc/04 - Flows/Registration Flow.md
Siavash Sameni 7a616744f4 docs: complete code-reality alignment for remaining docs + reconcile issue set
Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
  Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
  added undocumented endpoints (ton-proof challenge, profile email verify,
  GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
  enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
  90-day notification TTL, soft-delete semantics, wallet fields

Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
  page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
  TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation

Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
  ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:15:02 +04:00

204 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Registration Flow
tags: [flow, auth, signup, email-verification, referral]
related_models: ["[[User]]", "[[TempVerification]]"]
related_apis: ["POST /api/auth/register", "POST /api/auth/verify-email-code", "POST /api/auth/resend-verification"]
---
# Registration Flow
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
End-to-end specification for **email + password** registration with role selection (buyer/seller), six-digit email verification, optional referral code attribution, and terms acceptance.
## Actors
- **Prospective User** submits the sign-up form.
- **Frontend** `frontend/src/auth/view/jwt/jwt-sign-up-view.tsx`, calling `signUp()` and later `verifyEmailWithCode()` from `frontend/src/auth/context/jwt/action.ts`.
- **Backend** `AuthController.register` and `AuthController.verifyEmailWithCode` in `backend/src/services/auth/authController.ts`.
- **MongoDB** `TempVerification` collection (temporary), then `User` collection (final).
- **Email service** `backend/src/services/email/emailService.ts` (SMTP/transactional provider) — `sendVerificationCodeEmail()`.
- **Socket.IO** emits `referral-signup` to the referrer if a referral code is supplied.
## Preconditions
- The email is not already a verified `User`. If a `TempVerification` already exists, its code and metadata are **regenerated and resent** rather than throwing a conflict.
- Outbound SMTP credentials are configured (`EMAIL_*` env vars consumed by `emailService.ts`).
- If a `referralCode` is supplied, it does **not** need to exist for sign-up to succeed — invalid codes are silently ignored at verification time.
## State machine: `TempVerification → User`
```mermaid
stateDiagram-v2
[*] --> NotStarted
NotStarted --> TempCreated: POST /api/auth/register\nemail + role [+ ref]
TempCreated --> TempCreated: POST /api/auth/resend-verification\n(new code, 15-min TTL)
TempCreated --> TempExpired: 15 minutes elapse\nor verification fails
TempExpired --> TempCreated: User clicks "Resend"
TempCreated --> UserActive: POST /api/auth/verify-email-code\n(code + password)
UserActive --> [*]
note right of TempCreated
TempVerification document holds:
email, firstName, lastName, role,
referralCode, code, codeExpires
end note
note right of UserActive
User created with isEmailVerified=true,
status="active"; tokens issued immediately.
end note
```
## Step-by-step narrative
### Phase 1 — Submit registration
1. **User visits `/auth/jwt/sign-up`** (optionally with `?ref=ABCD1234` from the short-URL referral redirect implemented at `backend/src/app.ts:274-278`).
2. **User selects role** (buyer or seller), enters email, password (held in client state only), accepts the Terms checkbox, and clicks "Create account".
> [!bug] ⚠️ KNOWN BUG / quirk — the sign-up form does not collect the real password
> `jwt-sign-up-view.tsx` `onSubmit` calls `signUp({ ..., password: '' })` with a **hard-coded empty string** (`jwt-sign-up-view.tsx:191`, with the inline comment `// You might need to add password field to form`). So the actual password is **not** collected on the sign-up form at all — it is collected at the **email-verification step** (`/verify-email-code`). The `TempVerification.password` field is effectively **unused** (it is set to `''` and never read as a real credential). The credential that ends up on the `User` is the one entered at verification.
3. **HTTP request**: `POST /api/auth/register` with `{ email, password: '', firstName?, lastName?, role, referralCode? }`. The frontend passes `password: ''` (empty string) — see the quirk above. The controller persists this empty string into `TempVerification.password`, which is never used as a real credential.
4. **Validation middleware** `registerValidation` (`authValidation.ts`) checks email format, password complexity, and role enum.
5. **Duplicate check** (`authController.ts:55-64`): `User.findOne({ email })` — if found, returns `409 USER_EXISTS`.
6. **Idempotent temp record**: `TempVerification.findOne({ email })` — if present, the existing temp is **updated in place** (new name, role, referralCode, fresh 6-digit code, expiry pushed to now + 15 min).
7. **Verification code**: `authService.generateVerificationCode()` (`authService.ts:226-228`) returns a uniformly random 6-digit string.
8. **Persistence**: A new `TempVerification` is saved with `{ email, password: '', firstName: defaults to "کاربر", lastName: defaults to "جدید", role, referralCode, emailVerificationCode, emailVerificationCodeExpires }`.
9. **Email dispatch**: `emailService.sendVerificationCodeEmail(email, firstName, code)` is called. The email contains the 6-digit code, branding, and a 15-minute expiry notice. Failure to send is logged but the response still succeeds with `201` (the user can resend).
10. **Response**: `{ email, message: "Verification code sent to email" }` with HTTP `201` for first-time, `200` for resend.
11. **Frontend** transitions to the OTP screen `/auth/jwt/verify?email=...` (`frontend/src/auth/view/jwt/jwt-verify-view.tsx`).
### Phase 2 — Verify code and finalise
12. **User enters the 6-digit code** and confirms the password. The password may be re-entered here for safety.
13. **HTTP request**: `POST /api/auth/verify-email-code` with `{ email, code, password }`.
14. **Format guard**: `authService.isValidVerificationCode(code)` enforces `/^\d{6}$/` (`authService.ts:236-238`).
15. **Lookup**: `TempVerification.findOne({ email, emailVerificationCode: code, emailVerificationCodeExpires: { $gt: now } })` — if any field mismatches or the code is older than 15 minutes, returns `400`.
16. **Hash password**: `bcrypt.hash(password, 12)` via `authService.hashPassword()`.
17. **Create `User`** (`authController.ts:400-435`): `email`, `password: hashedPassword`, `firstName`, `lastName`, `role`, `isEmailVerified: true`, `status: "active"`.
18. **Apply referral** (`authController.ts:691-713`): `tempVerification.referralCode` (stored on the `TempVerification` document at registration and applied here at verification) is looked up via `User.findOne({ referralCode })`. If a referrer is found:
- `user.referredBy = referrer._id`
- `referrer.referralStats.totalReferrals += 1`
- Emit `referral-signup` on `user-${referrer._id}` Socket.IO room (`authController.ts:704`; the equivalent Google/other path emits at `authController.ts:1132`) — see [[Referral Flow]] for the points-awarding side effect that happens later on the first purchase.
- ⚠️ **No self-referral guard**: the code only checks `if (referrer)` — it never compares `referrer._id` to the newly created user. A user who somehow signs up with their own `referralCode` would be attributed as their own referrer.
19. **Persist user**, then **delete** the TempVerification document (`findByIdAndDelete`).
20. **Token issuance**: identical to [[Authentication Flow]] — generate access + refresh, push the refresh into `user.refreshTokens[]`.
21. **Response**: `{ user, tokens: { accessToken, refreshToken } }`. Frontend writes both into `localStorage` (`action.ts:228-235`) and routes the user into the appropriate dashboard (`/dashboard/buyer` or `/dashboard/seller`).
## Sequence diagram
```mermaid
sequenceDiagram
autonumber
actor U as User
participant FE as Frontend
participant BE as Backend
participant DB as MongoDB
participant MAIL as Email Service
participant IO as Socket.IO
U->>FE: Fill sign-up form (email, role, ref?, password)
FE->>BE: POST /api/auth/register
BE->>DB: User.findOne({ email })
DB-->>BE: null
BE->>DB: TempVerification.findOne({ email })
DB-->>BE: null
BE->>BE: code = generateVerificationCode()
BE->>DB: TempVerification.create({...code, expires=+15m})
BE->>MAIL: sendVerificationCodeEmail(email, firstName, code)
MAIL-->>U: Email with 6-digit code
BE-->>FE: 201 { email, message }
FE-->>U: Redirect /auth/jwt/verify
U->>FE: Enter code + (re)password
FE->>BE: POST /api/auth/verify-email-code { email, code, password }
BE->>DB: TempVerification.findOne({ email, code, expires>now })
DB-->>BE: tempVerification doc
BE->>BE: hashPassword(password)
BE->>DB: User.create({...isEmailVerified:true, status:active})
opt referral present
BE->>DB: User.findOne({ referralCode })
DB-->>BE: referrer
BE->>DB: referrer.referralStats.totalReferrals += 1
BE->>IO: emit user-{refId} 'referral-signup'
end
BE->>DB: TempVerification.findByIdAndDelete(...)
BE->>BE: generate tokens
BE->>BE: push refresh
BE-->>FE: 200 { user, tokens }
FE->>FE: localStorage.setItem(accessToken, refreshToken)
FE-->>U: Redirect /dashboard/{role}
```
## API calls
| Method | Endpoint | Source |
|---|---|---|
| `POST` | `/api/auth/register` | `authRoutes.ts:21``authController.register` |
| `POST` | `/api/auth/verify-email-code` | `authRoutes.ts:34``authController.verifyEmailWithCode` |
| `POST` | `/api/auth/resend-verification` | `authRoutes.ts:36-40``authController.resendVerificationEmail` |
| `GET` | `/r/:code` | `app.ts:274-278` — short-URL redirect that injects `?ref=` into the sign-up page |
| `POST` | `/api/auth/force-verify-user` | Dev-only — `authController.forceVerifyUser` (rejects outside `NODE_ENV=development`) |
## Database writes
- **`tempverifications` collection**: insert on first POST (carrying `email`, `password: ''`, `firstName`, `lastName`, `role`, `referralCode`, code + expiry), in-place update on duplicate POST, delete on successful verification.
- **`users` collection**: full insert on successful verification (`authController.ts:680-688`). The first refresh token is appended in the same save.
- **`users` collection (referrer)**: `referralStats.totalReferrals` incremented (`authController.ts:699`).
## Socket events emitted
- **`referral-signup`** → `user-${referrerId}` room when a referred user verifies. Payload:
```
{ userId, userName, userEmail, timestamp, totalReferrals }
```
Source: `authController.ts:704-710` (and `:1132` on the parallel path).
## Side effects
- **Email**: one transactional message per `/register` and per `/resend-verification`. Content is generated by `emailService.sendVerificationCodeEmail`. Plain-text fallback included.
- **Sentry**: errors during `User.create` or email dispatch are captured server-side.
- **Logs**: the controller `console.log`s the generated code in **all environments** (`authController.ts:88`, `:117`, `:518`). Useful in dev; in prod the same log line ends up in CloudWatch/Sentry breadcrumbs. (Tracked as a hardening item.)
> [!warning] Verification code is logged server-side
> The generated 6-digit code is `console.log`-ed by the controller even in production. Anyone with log access can take over an unverified account. Move behind `if (NODE_ENV !== 'production')`.
## Error / edge cases
- **Email already registered (verified)** → `409 USER_EXISTS`.
- **Email already in temp (unverified)** → `200`, code regenerated, email re-sent. User-friendly; no error.
- **Code mismatch / expired (>15 min)** → `400 Invalid or expired verification code`. The TempVerification is **not** deleted, so the user can request a new code via "Resend".
- **Code format wrong (non-digits or wrong length)** → `400` from `isValidVerificationCode` guard before DB lookup.
- **Email delivery failure** → response still `201`/`200`; the user can hit "Resend" or check spam.
- **Referral code that does not match any user** → silently ignored; the user is still created with `referredBy: undefined`.
- **Self-referral** → **not guarded**. The referral attribution (`authController.ts:691-713`) only checks that a referrer exists, never that it differs from the signing-up user.
- **Race condition: two parallel registrations for the same email** → MongoDB unique index on `User.email` ensures only one user document; the loser of the race sees `E11000` and returns `409 USER_EXISTS`.
- **Race condition: verify request arrives twice with the same code** → second request finds no TempVerification and returns `400`. The created `User` is the canonical record.
- **Role tampering** → role is validated by `registerValidation` enum (`buyer | seller`). Admin role is created only via the bootstrap seed (`initializeAdminUser` in `app.ts:377`), never via this flow.
## Defaults & quirks
- `firstName` / `lastName` are not required by the frontend in many sign-up variants; the controller defaults them to Persian placeholders `"کاربر"` / `"جدید"` (`authController.ts:52-53`). They can be edited later under `/dashboard/account/profile`.
- The TempVerification TTL is enforced by the `emailVerificationCodeExpires` check, not by a Mongo TTL index — expired docs remain in the collection until overwritten or manually purged.
## Linked flows
- [[Authentication Flow]] — the next time the user signs in (includes the Telegram first-class auth section).
- [[Referral Flow]] — full points-awarding mechanics triggered here.
- [[Google OAuth Flow]] — alternative path that bypasses `TempVerification` (Google identities are pre-verified).
- [[Password Reset Flow]] — if the user forgets the password they set during verification.
> [!tip] Telegram — zero-step registration
> Users who open the Amanat Telegram Mini App do **not** go through this flow at all. `POST /api/auth/telegram` verifies the Telegram-signed `initData` and auto-provisions a new `User` (no email, `authProvider: "telegram"`) in a single round-trip. The `TempVerification` + email code cycle only applies to email-based sign-ups. See [[Authentication Flow#Telegram first-class auth flow]].
## Source files
- Backend: `backend/src/services/auth/authController.ts:33-158` (register), `:364-469` (verify), `:498-539` (resend)
- Backend: `backend/src/services/auth/authValidation.ts` (validation rules)
- Backend: `backend/src/models/TempVerification.ts` (temp schema)
- Backend: `backend/src/services/email/emailService.ts` (`sendVerificationCodeEmail`)
- Backend: `backend/src/app.ts:274-278` (short referral redirect)
- Frontend: `frontend/src/auth/view/jwt/jwt-sign-up-view.tsx`
- Frontend: `frontend/src/auth/view/jwt/jwt-verify-view.tsx`
- Frontend: `frontend/src/auth/context/jwt/action.ts:121-256`