Authentication Live
Email + password + OTP verification. Three account types — Consumer, Seller, Doctor — each with their own token storage convention.
1. Overview
Sign up → email verification (6-digit OTP, 10-min expiry, 60s resend cooldown) → JWT issued. Forgot password → 32-byte hex token via email link → 15-min expiry → reset. All emails go via the central email lib (noreply@ka-26.com).
2. User journey
Sign up
- Go to auth page (consumer / seller / doctor variant)
- Enter email + password + name
- Submit → backend creates row + sends OTP email
- Enter 6-digit OTP from email → verify → JWT issued
Forgot password (all 3 user types)
- Click "Forgot password?"
- Enter email + select type (consumer / seller / doctor)
- Generic success response (prevents email enumeration)
- Receive password reset email with 15-min link
- Click link → set new password → log in
3. Web ↔ Mobile parity
| Capability | Web | Mobile |
|---|---|---|
| Consumer signup | ✅ /consumer-auth | ✅ auth.tsx |
| Seller signup | ✅ /seller-auth | ❌ Seller is web-only |
| Doctor signup | ✅ /doctor-auth | ❌ Doctor is web-only |
| Email verification (OTP) | ✅ all 3 types | ✅ consumer only |
| Forgot password | ✅ all 3 types | ✅ consumer only |
| Google OAuth | ✅ Consumer + Seller + Doctor | ✅ Consumer (Expo Auth Session) |
| Reset password | ✅ all 3 types | ✅ consumer only |
4. Key components
Web
src/app/(auth)/consumer-auth/page.tsx— consumer signup/login UIsrc/app/(auth)/consumer-reset-password/page.tsx— reset formsrc/app/seller/login/page.tsx— sellersrc/app/seller/reset-password/page.tsxsrc/app/doctor/page.tsx— doctor portal entry
Mobile
mobile/app/auth.tsx— consumer signup/loginmobile/contexts/AuthContext.tsx— state + token persistence
Shared backend
src/lib/auth.ts— JWT signing/verification, password hashing (bcrypt, salt 12)src/lib/email.ts—sendEmailVerification,sendPasswordResetsrc/lib/security.ts— rate limiting, IP detection, sanitization
5. APIs
| Endpoint | Method | Purpose |
|---|---|---|
/api/auth/consumer-register | POST | Create consumer + send OTP |
/api/auth/consumer-login | POST | Email/password login (issues JWT) |
/api/auth/register | POST | Seller signup |
/api/auth/login | POST | Seller login |
/api/doctor/auth | POST | Doctor signup/login (single endpoint with action field) |
/api/auth/send-verification | POST | Send OTP (rate-limited, 60s per-user cooldown) |
/api/auth/verify-email | POST | Verify OTP. Returns reason: "invalid" | "used" | "expired" for precise UI messaging |
/api/auth/forgot-password | POST | Generate reset token + email link. Body: { email, type: "consumer" | "seller" | "doctor" } |
/api/auth/reset-password | POST | Apply new password using token. Resolves token across all 3 user tables |
/api/auth/google | POST | Google OAuth callback (consumer) |
/api/auth/seller-google | POST | Google OAuth (seller) |
/api/auth/doctor-google | POST | Google OAuth (doctor) |
6. Database touchpoints
Consumer,Seller,Doctor— each haspasswordHash,email,emailVerified,phoneVerified,resetToken,resetTokenExp,googleIdVerificationCode— polymorphic OTP store withconsumerId/sellerId/doctorId(one set),type(email/phone),code,target,expiresAt,usedAtEmailLog— every email send tracked (status, attempts, error)
7. Business logic
Token storage conventions
- Consumer:
localStorage["ka26_consumer_token"](web) / SecureStore (mobile) - Seller: httpOnly cookie
seller_token - Doctor:
localStorage["ka26_doctor_token"] - Delivery Partner:
localStorage["ka26_delivery_token"]
Why different storage?
- Seller uses httpOnly cookie because seller auth is more sensitive (financial dashboard) and cookie auth survives subdomain redirects cleanly
- Consumer uses localStorage because mobile React Native can't share cookies with the web view; keeping it in JS-readable storage means same code in both
OTP rules
- 6 digits (
generateOTP()insrc/lib/email.ts) - 10-min expiry (
OTP_EXPIRY_MS) - Per-user resend cooldown: 60s (
OTP_RESEND_COOLDOWN_MS) - Per-IP rate limit: 3 sends per 5 min (
/api/auth/send-verification) - Verify endpoint distinguishes
invalid/used/expiredin the response — UI shows actionable messages
Password reset
- 32-byte hex token (
crypto.randomBytes(32).toString("hex")) - 15-min expiry (
PASSWORD_RESET_EXPIRY_MS) - Reset link embeds
?type=so the frontend renders the right reset page - Token resolved across all 3 user tables (seller → consumer → doctor)
- Generic "if an account exists..." response prevents enumeration
Password rules
- Consumer: min 6 chars (
PASSWORD_MIN_LENGTH_CONSUMER) - Seller: min 8 chars (
PASSWORD_MIN_LENGTH_SELLER) - bcrypt with
SALT_ROUNDS = 12
8. Feature flags / env vars
JWT_SECRET— REQUIRED, must be the same across all running instancesGOOGLE_CLIENT_ID+GOOGLE_CLIENT_SECRET— optional; OAuth buttons hidden if not set- SMTP env vars — for OTP + reset emails (see Email Infra)
9. Tests
tests/email-infrastructure.test.ts— 37 tests including OTP flow, reset flow, all-3-types coveragetests/seller-verification.test.tstests/google-oauth.test.ts
10. Known gotchas
- Doctor model
resetTokenwas added 2026-04-17 — earlier doctor accounts couldn't reset password. Migration20260417_doctor_password_reset/migration.sqlapplied to prod. - Seller
emailis the admin auth check —/api/website/jobsroutes hardcodeseller.email !== "siddugkattimani@gmail.com". Don't change without RBAC redesign. - OTP table is polymorphic: exactly one of
consumerId/sellerId/doctorIdmust be set. Trying to set two will succeed at the DB level (no constraint) but your code will be confused — add a check. - Mobile auth uses Expo SecureStore, NOT AsyncStorage. Tokens are in the iOS Keychain / Android Keystore. Survives app reinstall.
11. Related
- Email Infrastructure (OTP, reset emails)
- Notifications (push token registration happens at auth time)
- CHANGELOG: 2026-04-17 email migration