196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
---
|
||
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; 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.
|
||
- [[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.
|
||
|
||
## 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`
|