Skip to main content

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

  1. Go to auth page (consumer / seller / doctor variant)
  2. Enter email + password + name
  3. Submit → backend creates row + sends OTP email
  4. Enter 6-digit OTP from email → verify → JWT issued

Forgot password (all 3 user types)

  1. Click "Forgot password?"
  2. Enter email + select type (consumer / seller / doctor)
  3. Generic success response (prevents email enumeration)
  4. Receive password reset email with 15-min link
  5. Click link → set new password → log in

3. Web ↔ Mobile parity

CapabilityWebMobile
Consumer signup/consumer-authauth.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 UI
  • src/app/(auth)/consumer-reset-password/page.tsx — reset form
  • src/app/seller/login/page.tsx — seller
  • src/app/seller/reset-password/page.tsx
  • src/app/doctor/page.tsx — doctor portal entry

Mobile

  • mobile/app/auth.tsx — consumer signup/login
  • mobile/contexts/AuthContext.tsx — state + token persistence

Shared backend

  • src/lib/auth.ts — JWT signing/verification, password hashing (bcrypt, salt 12)
  • src/lib/email.tssendEmailVerification, sendPasswordReset
  • src/lib/security.ts — rate limiting, IP detection, sanitization

5. APIs

EndpointMethodPurpose
/api/auth/consumer-registerPOSTCreate consumer + send OTP
/api/auth/consumer-loginPOSTEmail/password login (issues JWT)
/api/auth/registerPOSTSeller signup
/api/auth/loginPOSTSeller login
/api/doctor/authPOSTDoctor signup/login (single endpoint with action field)
/api/auth/send-verificationPOSTSend OTP (rate-limited, 60s per-user cooldown)
/api/auth/verify-emailPOSTVerify OTP. Returns reason: "invalid" | "used" | "expired" for precise UI messaging
/api/auth/forgot-passwordPOSTGenerate reset token + email link. Body: { email, type: "consumer" | "seller" | "doctor" }
/api/auth/reset-passwordPOSTApply new password using token. Resolves token across all 3 user tables
/api/auth/googlePOSTGoogle OAuth callback (consumer)
/api/auth/seller-googlePOSTGoogle OAuth (seller)
/api/auth/doctor-googlePOSTGoogle OAuth (doctor)

6. Database touchpoints

  • Consumer, Seller, Doctor — each has passwordHash, email, emailVerified, phoneVerified, resetToken, resetTokenExp, googleId
  • VerificationCode — polymorphic OTP store with consumerId / sellerId / doctorId (one set), type (email / phone), code, target, expiresAt, usedAt
  • EmailLog — 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() in src/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 / expired in 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 instances
  • GOOGLE_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 coverage
  • tests/seller-verification.test.ts
  • tests/google-oauth.test.ts

10. Known gotchas

  • Doctor model resetToken was added 2026-04-17 — earlier doctor accounts couldn't reset password. Migration 20260417_doctor_password_reset/migration.sql applied to prod.
  • Seller email is the admin auth check/api/website/jobs routes hardcode seller.email !== "siddugkattimani@gmail.com". Don't change without RBAC redesign.
  • OTP table is polymorphic: exactly one of consumerId / sellerId / doctorId must 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.