Skip to content

feat(auth): Email OTP verification service#19

Merged
codebestia merged 4 commits into
ShadeProtocol:mainfrom
stiven-skyward:issue-10-email-otp
Jun 28, 2026
Merged

feat(auth): Email OTP verification service#19
codebestia merged 4 commits into
ShadeProtocol:mainfrom
stiven-skyward:issue-10-email-otp

Conversation

@stiven-skyward

@stiven-skyward stiven-skyward commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add server-side 6-digit email OTP generation with bcrypt hash and 10-minute expiry on merchant registration
  • Add email.service.ts with Resend/SMTP/console providers and email template including merchant first name
  • Add protected POST /auth/verify-email and POST /auth/resend-otp endpoints (resend rate-limited to 1/min)
  • Add Prisma migration for emailOtp and emailOtpExpiresAt on Merchant

closes #10

Test plan

  • npm test — 115/115 tests passing locally
  • Registration stores OTP hash and triggers email send
  • Verify-email: correct code → 200 + emailVerified: true; wrong/expired → 400
  • Resend-otp: regenerates code; blocks requests within 1 minute
  • OTP hash cleared from DB after successful verification

Summary by CodeRabbit

  • New Features
    • Added merchant-protected email OTP endpoints: POST /verify-email and POST /resend-otp.
    • Implemented time-limited OTP email verification for merchant registration.
    • Added configurable email delivery options (console, Resend, SMTP) and expanded environment variable templates.
  • Bug Fixes
    • Improved OTP security with hashed codes, expiry validation, and resend cooldown/rate limiting.
  • Tests
    • Added/updated integration and unit tests for verify, resend, expiry, and cooldown behavior.
  • Chores
    • Updated lint configuration to ignore the ESLint config file.

Move OTP generation from client to backend with bcrypt-hashed codes, email delivery via Resend/SMTP, and protected verify-email and resend-otp endpoints.

Closes ShadeProtocol#10

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f334697-1b35-42f7-9e00-90d066880f19

📥 Commits

Reviewing files that changed from the base of the PR and between 4d100ad and 354461b.

📒 Files selected for processing (1)
  • src/services/otp.services.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/services/otp.services.ts

📝 Walkthrough

Walkthrough

Server-generated merchant email OTP support is added. The PR introduces email provider configuration, Merchant OTP persistence fields, OTP email dispatch, verification and resend flows, protected auth endpoints, and updated registration and integration tests.

Changes

Merchant email OTP flow

Layer / File(s) Summary
Configuration and schema
.env.example, package.json, eslint.config.cjs, src/config/environment.ts, prisma/migrations/20260623120000_add_merchant_email_otp/migration.sql, prisma/schema.prisma
Email provider environment variables, runtime dependencies, lint ignores, and Merchant OTP columns are added.
Email delivery service
src/services/email.service.ts
The email service builds OTP message content and sends it through Resend, SMTP, or console logging.
OTP service
src/services/otp.services.ts, tests/unit/otp.services.test.ts
The OTP service hashes codes, persists expiry metadata, verifies submitted codes, and reissues codes with resend cooldown checks; unit tests cover verification and resend behavior.
Merchant registration OTP write path
src/services/merchant.services.ts, tests/unit/merchant.register.test.ts, tests/integration/merchant.register.test.ts
Merchant registration now generates an OTP, stores the hash and expiry, and sends the plaintext code by email; unit and integration tests were updated for the new mail call and persisted fields.
Email verification routes
src/controllers/auth.controllers.ts, src/routes/auth.routes.ts, tests/integration/auth.email-otp.test.ts
The auth controller adds verify-email and resend-otp handlers, and the auth router exposes both protected endpoints; integration tests cover success and error cases.

Sequence Diagram(s)

sequenceDiagram
  participant MerchantClient
  participant auth.routes
  participant verifyEmailController
  participant verifyEmailOtp
  participant prisma

  MerchantClient->>auth.routes: POST /verify-email { code }
  auth.routes->>verifyEmailController: authenticateMerchant
  verifyEmailController->>verifyEmailOtp: verifyEmailOtp(merchant.id, code)
  verifyEmailOtp->>prisma: findUnique / update emailVerified, emailOtp, emailOtpExpiresAt
  verifyEmailController-->>MerchantClient: 200 sanitized merchant
Loading
sequenceDiagram
  participant MerchantClient
  participant auth.routes
  participant resendOtpController
  participant resendEmailOtp
  participant prisma
  participant sendOtp

  MerchantClient->>auth.routes: POST /resend-otp
  auth.routes->>resendOtpController: authenticateMerchant
  resendOtpController->>resendEmailOtp: resendEmailOtp(merchant.id)
  resendEmailOtp->>prisma: check OTP state / update emailOtp, emailOtpExpiresAt
  resendEmailOtp->>sendOtp: sendOtp(email, code, firstName)
  resendOtpController-->>MerchantClient: 200 Verification code sent
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • codebestia

Poem

I hopped a code from server moss,
And sent it off without a loss.
I verified the tiny spark,
Then called for more before it’s dark.
🐇📧✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The ESLint config change to ignore eslint.config.cjs is unrelated to the OTP/email work and appears out of scope. Remove the lint-ignore tweak unless it is needed for a separate, documented linting fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: backend email OTP verification for auth.
Linked Issues check ✅ Passed The PR matches the linked issue: it adds server-side OTP generation, email delivery, verify/resend routes, hashing, expiry, and clears OTP after success.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/merchant.services.ts (1)

114-131: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Email send happens after the registration commit; a delivery failure leaves the merchant in a confusing state.

prisma.merchant.update commits registered: true plus the OTP hash, and only then is sendOtp awaited. If sendOtp throws (Resend/SMTP outage), registerMerchant rejects and the client sees an error, but the record is already persisted as registered. A retry then hits the earlier merchant.registered guard and returns 409 'Profile already set up', so the user never receives a code from this path (recovery only via resend-otp).

Consider not failing registration on a transient mail error (log and let the user resend), or moving delivery into a flow where a failure is surfaced as "registered, but resend the code" rather than a hard 500.

♻️ One option: don't reject registration on mail failure
-  await sendOtp(normalizedEmail, code, data.firstName.trim());
-
-  return sanitizeMerchant(updatedMerchant);
+  try {
+    await sendOtp(normalizedEmail, code, data.firstName.trim());
+  } catch (err) {
+    // Registration is already committed; surface via resend-otp instead of failing the request.
+    console.error('Failed to send OTP email after registration', err);
+  }
+
+  return sanitizeMerchant(updatedMerchant);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/merchant.services.ts` around lines 114 - 131, The registration
flow in registerMerchant persists the merchant as registered before calling
sendOtp, so a mail outage leaves the record committed but the request failing.
Update the registerMerchant flow around prisma.merchant.update and sendOtp so a
transient OTP delivery failure does not reject the whole registration; instead
log the sendOtp error and return a response that tells the user the profile is
set up and they should resend the code, or otherwise separate persistence from
delivery so retries do not hit the merchant.registered guard.
🧹 Nitpick comments (2)
src/config/environment.ts (1)

24-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Provider value is cast, not validated.

process.env.EMAIL_PROVIDER is force-cast to EmailProvider, so a misconfigured value (e.g. resnd) compiles fine and silently falls through to the console branch in email.service.ts, meaning real OTP emails are never sent while the app appears healthy. Consider validating against the allowed set at startup and failing fast (or logging a warning).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config/environment.ts` around lines 24 - 35, The EMAIL_PROVIDER setting
in environment configuration is being force-cast instead of validated, so
invalid values can silently fall back to the console email path. Update the
configuration logic in environment.ts for the email.provider field to validate
process.env.EMAIL_PROVIDER against the allowed EmailProvider values before
exposing it, and either fail fast at startup or emit a clear warning when the
value is invalid; use the EmailProvider type and the email.service.ts provider
switch as the key places to align behavior.
src/services/email.service.ts (1)

5-16: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Unescaped firstName interpolated into email HTML.

firstName is injected directly into the HTML body. Since it is the merchant's own value delivered to the merchant's own address, the risk is contained, but HTML-escaping it would prevent broken markup and harden against future reuse with attacker-influenced names.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/email.service.ts` around lines 5 - 16, The OTP email builder
currently interpolates firstName directly into the HTML in buildOtpEmailContent,
so update that path to HTML-escape the name before inserting it while keeping
the text body unchanged. Use the existing buildOtpEmailContent helper as the fix
point, and make sure the escaped value is used in the html template so future
reuse of this function cannot break markup or allow injected HTML.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/otp.services.ts`:
- Around line 11-15: The OTP generator in generateOtp currently relies on
Math.random(), which is not secure for verification codes. Update generateOtp to
use a cryptographically secure source such as crypto.randomInt to produce the
numeric OTP, while preserving the existing OTP_LENGTH-based range and string
return value. Keep the change localized to generateOtp and any related OTP
constants/helpers used to compute the bounds.

---

Outside diff comments:
In `@src/services/merchant.services.ts`:
- Around line 114-131: The registration flow in registerMerchant persists the
merchant as registered before calling sendOtp, so a mail outage leaves the
record committed but the request failing. Update the registerMerchant flow
around prisma.merchant.update and sendOtp so a transient OTP delivery failure
does not reject the whole registration; instead log the sendOtp error and return
a response that tells the user the profile is set up and they should resend the
code, or otherwise separate persistence from delivery so retries do not hit the
merchant.registered guard.

---

Nitpick comments:
In `@src/config/environment.ts`:
- Around line 24-35: The EMAIL_PROVIDER setting in environment configuration is
being force-cast instead of validated, so invalid values can silently fall back
to the console email path. Update the configuration logic in environment.ts for
the email.provider field to validate process.env.EMAIL_PROVIDER against the
allowed EmailProvider values before exposing it, and either fail fast at startup
or emit a clear warning when the value is invalid; use the EmailProvider type
and the email.service.ts provider switch as the key places to align behavior.

In `@src/services/email.service.ts`:
- Around line 5-16: The OTP email builder currently interpolates firstName
directly into the HTML in buildOtpEmailContent, so update that path to
HTML-escape the name before inserting it while keeping the text body unchanged.
Use the existing buildOtpEmailContent helper as the fix point, and make sure the
escaped value is used in the html template so future reuse of this function
cannot break markup or allow injected HTML.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b6cb7543-bfc8-47be-b6ff-787a5b43195a

📥 Commits

Reviewing files that changed from the base of the PR and between c0e30a5 and 8beece7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • .env.example
  • package.json
  • prisma/migrations/20260623120000_add_merchant_email_otp/migration.sql
  • prisma/schema.prisma
  • src/config/environment.ts
  • src/controllers/auth.controllers.ts
  • src/routes/auth.routes.ts
  • src/services/email.service.ts
  • src/services/merchant.services.ts
  • src/services/otp.services.ts
  • tests/integration/auth.email-otp.test.ts
  • tests/integration/merchant.register.test.ts
  • tests/unit/merchant.register.test.ts
  • tests/unit/otp.services.test.ts

Comment thread src/services/otp.services.ts
stiven-skyward and others added 2 commits June 25, 2026 14:53
Use crypto.randomInt for OTP generation, handle email send failures gracefully after registration, validate EMAIL_PROVIDER, escape HTML in email template, and rename eslint.config to .cjs for ESM compatibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@stiven-skyward

Copy link
Copy Markdown
Contributor Author

Hi! I've addressed all CodeRabbit feedback in commits 7faed92 and 4d100ad. All 115 tests pass locally. Ready for maintainer review whenever you have time. Thanks!

@codebestia

Copy link
Copy Markdown
Contributor

Hello @stiven-skyward
Apologies for the late review.
Please fix the CI issues.

Co-authored-by: Cursor <cursoragent@cursor.com>
@stiven-skyward

Copy link
Copy Markdown
Contributor Author

Hello @codebestia please check again.

@codebestia codebestia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!
Thank you for your contribution.

@codebestia codebestia merged commit 971a528 into ShadeProtocol:main Jun 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Email OTP Verification Service

2 participants