Skip to main content

Sign-up Email OTP

This document describes the email OTP verification at sign-up: before an account is created, the user must prove they can receive email at the given address by entering a 6-digit code sent via Resend. This prevents dummy or fake email addresses from being used for registration. Sign-up and login OTP are email only (no SMS/MSG91 in the current codebase).


Overview

AreaBehaviour
Flow3 steps: (1) Enter email → Send code, (2) Enter 6-digit OTP → Verify, (3) Complete registration form with email locked.
BackendNew table signup_otps; endpoints POST /register/send-otp, POST /register/verify-otp; POST /register requires a short-lived signup token (unless bypassed).
EmailOTP is sent using the same Resend integration as password reset and login 2FA.
ClientsWeb (customer register), customer Android app, and Windows shopkeeper app all use the same 3-step flow.
Login OTPLogin can also use email OTP (POST /login/request-otp, POST /login/verify-otp). OTP is email only (no SMS). See API Documentation.

User flow (all clients)

  1. Step 1 – Email and send OTP

    • User enters email (and on web, selects role: customer or shopkeeper).
    • User taps/clicks Send verification code.
    • Backend checks that the email is not already registered, then sends a 6-digit OTP to that address via Resend.
  2. Step 2 – Verify OTP

    • UI shows: “We sent a 6-digit code to user@example.com. Enter it below.”
    • User enters the 6-digit code and taps Verify.
    • Backend verifies the code and returns a signup token (and the verified email). No account is created yet.
  3. Step 3 – Complete registration

    • Email is shown as verified and read-only.
    • User fills the rest of the form (name, username, phone, password, and for shopkeeper: shop name, address, location).
    • On submit, the client sends signup_token with the registration payload. Backend validates the token and that the body email matches the token, then creates the user.

Backend

Database

  • Table signup_otps
    • email_lower (PK), code_hash, expires_at
    • One active OTP per email; sending a new code replaces the previous one.
    • OTP expires after 10 minutes.

Endpoints

MethodPathDescription
POST/register/send-otpBody: { "email": "user@example.com" }. Validates format, rejects if email already registered, generates 6-digit OTP, stores hash, sends email via Resend. Returns 200 with { "message": "Verification code sent to your email." }.
POST/register/verify-otpBody: { "email", "code": "123456" }. Verifies code and expiry, deletes OTP (one-time use), issues signup token. Returns 200 with { "signup_token": "...", "email": "user@example.com" }.
POST/registerSame body as before, plus signup_token. Backend validates token and requires req.Email to match the token’s email, then runs existing validation and creates the user.

All three are in the same auth rate-limit group (e.g. 50 requests per 15 min per IP).

Signup token

  • Short-lived JWT (e.g. 15 minutes) with purpose signup_pending and email in claims.
  • Issued only by POST /register/verify-otp after successful OTP verification.
  • Valid only for one POST /register; backend does not store a separate “used” state (token expiry is sufficient).

Email template

  • SendSignupOTPEmail(to, code) – Subject: “Your Qprint Sign-up Code”. Body: 6-digit code and “Valid for 10 minutes.” Uses the same Resend configuration as other emails (RESEND_API_KEY, FROM_EMAIL).

Bypass (development / testing)

VariableDescription
SKIP_SIGNUP_OTPIf set to true, POST /register does not require signup_token. Use only in dev/test environments.

Configuration

  • Resend: Same as password reset and referral invites. Set RESEND_API_KEY and FROM_EMAIL (and optionally FROM_NAME).
  • JWT: Signup tokens use the same JWT_SECRET as login tokens.

Clients

Web (customer register)

  • Page: /register
  • API: registerSendOtp(email), registerVerifyOtp(email, code) → then POST /register with signup_token in the payload.
  • UI: Step 1 = email + role + “Send verification code”; Step 2 = OTP input + “Verify” + “Use a different email”; Step 3 = full form with “Email verified: …” (read-only) and “Create Account”.

Customer app (Android)

  • Screen: Register screen.
  • API: ApiService.registerSendOtp, ApiService.registerVerifyOtp, ApiService.register(..., signupToken).
  • UI: Same 3 steps; step 3 shows full form with email as “Email (verified)” read-only.

Shopkeeper app (Windows)

  • Screen: Register screen.
  • API: Same pattern: registerSendOtp, registerVerifyOtp, register(..., signupToken).
  • UI: Same 3 steps; step 3 is the full shopkeeper form (shop name, address, location, etc.) with email read-only.

Security and behaviour

  • Rate limiting: Auth group applies to /register/send-otp and /register/verify-otp (e.g. per-IP limit).
  • OTP: 6 digits, cryptographically random; stored as bcrypt hash; single use; 10-minute expiry.
  • Already registered: If the email exists in users, POST /register/send-otp returns 400 “This email is already registered.” and no OTP is sent.
  • Invalid/expired OTP: POST /register/verify-otp returns 400 “Invalid or expired code.” without revealing whether the email exists.
  • Token expired: If the user does not submit POST /register within the token lifetime, they must run “Send code” and “Verify” again.