Skip to content

Dedicated Email OTP Model (Corrects emailOTP Merchant-Field Approach) #28

Description

@codebestia

Background

The shipped email OTP verification by adding emailOtp/emailOtpExpiresAt directly onto the Merchant model. That's the wrong shape — a verification code isn't merchant profile data, a single pair of columns can't represent history, and it makes rate-limiting resend attempts (The implementation already specifies "1 resend per minute") awkward to enforce correctly. This issue moves OTP storage into its own model, shaped like the existing AuthNonce precedent (one row per generated code, with usedAt/expiresAt), rather than an upsert-in-place pair of fields.

Proposed Steps

  1. Add an EmailOtp model (see Schema Changes).
  2. Remove emailOtp and emailOtpExpiresAt from the Merchant model; run prisma migrate dev. Any outstanding unverified codes are lost across this migration — acceptable, nothing depends on preserving in-flight OTPs.
  3. Update otp.services.ts:
    • Code generation writes a new EmailOtp row (hashed code) instead of writing to Merchant
    • Verification looks up the most recent unused, unexpired EmailOtp for the merchant, compares the hash, and sets usedAt on success
    • Resend (POST /auth/resend-otp) creates a new EmailOtp row rather than overwriting fields on Merchant
  4. Enforce the existing "1 resend per minute" rule by checking for an EmailOtp row created for this merchant within the last 60 seconds before issuing a new one — this was awkward against a single mutable field and is now a normal query.
  5. No change to email.service.ts — only where the code is stored and checked changes, not how it's delivered.

Schema Changes

EmailOtp (new model)

id          String    (uuid, PK)
merchantId  String    (FK → Merchant)
codeHash    String    (hash of the 6-digit code — never store raw)
expiresAt   DateTime
usedAt      DateTime?
createdAt   DateTime

Merchant (remove fields)

emailOtp           — removed
emailOtpExpiresAt  — removed

Acceptance Criteria

  • EmailOtp model added; Merchant.emailOtp/emailOtpExpiresAt removed; prisma migrate dev runs cleanly
  • OTP codes are stored hashed, never in plaintext
  • POST /auth/verify-email checks against EmailOtp, not Merchant fields
  • POST /auth/resend-otp creates a new EmailOtp row and enforces the 1-per-minute limit against it
  • An expired or already-used EmailOtp row fails verification with the same 400 responses the current implementation already defined
  • No behavior change to email delivery itself — only storage/verification logic changes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions