Files
nick-doc/04 - Flows/Registration Flow.md
Siavash Sameni 940ad0c655 Add full system audit reports and Telegram Mini App debug handoff
- Three-stream audit (security / logic / performance) with 35+ findings
  derived from actual source code, each with file:line and remediation
- Audit Index cross-references criticals across streams into prioritized
  fix tiers: immediately / before soft launch / before public launch
- Telegram Mini App debug handoff documenting what was implemented and
  all remaining work items with exact file lists and test commands
- Updated architecture, data model, auth API, and registration flow docs
  to reflect Telegram auth, TON wallet, and email verification additions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:20:08 +04:00

200 lines
13 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
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".
> [!tip] Password is **not** sent to `/register`
> The password is only included in the second step (`/verify-email-code`). The intent: never hash and store a password for an unverified account. The TempVerification document carries `password: ''` until verification.
3. **HTTP request**: `POST /api/auth/register` with `{ email, password?, firstName?, lastName?, role, referralCode? }`. (The frontend currently passes the password through, but the controller stores `''` regardless — see `authController.ts:123`.)
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:411-433`): if `tempVerification.referralCode` exists, find the referrer by `User.findOne({ referralCode })`. If found:
- `user.referredBy = referrer._id`
- `referrer.referralStats.totalReferrals += 1`
- Emit `referral-signup` on `user-${referrer._id}` Socket.IO room — see [[Referral Flow]] for the points-awarding side effect that happens later on the first purchase.
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, in-place update on duplicate POST (`authController.ts:66-108`), delete on successful verification.
- **`users` collection**: full insert on successful verification (`authController.ts:400-435`). The first refresh token is appended in the same save.
- **`users` collection (referrer)**: `referralStats.totalReferrals` incremented (`authController.ts:419`).
## Socket events emitted
- **`referral-signup`** → `user-${referrerId}` room when a referred user verifies. Payload:
```
{ userId, userName, userEmail, timestamp, totalReferrals }
```
Source: `authController.ts:423-431`.
## 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`.
- **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`