Skip to content

feat(auth): add email verification code backend services#3051

Open
baktun14 wants to merge 6 commits intomainfrom
feat/auth0-verification-backend
Open

feat(auth): add email verification code backend services#3051
baktun14 wants to merge 6 commits intomainfrom
feat/auth0-verification-backend

Conversation

@baktun14
Copy link
Copy Markdown
Contributor

@baktun14 baktun14 commented Apr 9, 2026

Why

Part of CON-197 — Replace Auth0's default email verification link with a 6-digit code flow.

This is PR 1 of 2 — backend only. Split from #2824 to make review manageable.
PR 2 (frontend): #3052 (targets this branch).

What

  • DB migration: email_verification_codes table with unique index on userId (one row per user, upsert on new code)
  • EmailVerificationCodeRepository with Drizzle ORM upsert and SELECT ... FOR UPDATE
  • EmailVerificationCodeService with cooldown enforcement, code generation, and verification
  • Three new routes: POST /v1/send-verification-code, POST /v1/verify-email-code, POST /v1/signup
  • Auth0 service: createUser method for signup flow (409 sanitized to generic 422)
  • UserService: sends verification code on registration
  • Notification template for verification code emails
  • Auth0 markEmailVerified call moved outside transaction
  • HTTP SDK: sendVerificationCode() and verifyEmailCode() methods
  • Unit, integration, and functional tests

Auth0 Management API permissions

The management client needs these scopes on the Auth0 Management API (Dashboard → Applications → APIs → Auth0 Management API → Machine to Machine Applications):

Scope Used by
read:users users.get
update:users users.update (markEmailVerified)
create:users users.create (createUser with verify_email: false)

verify_email: false on users.create suppresses Auth0's built-in verification email for that specific call — needed because the tenant-wide email verification setting can't be toggled per-application.

Summary by CodeRabbit

  • New Features

    • Email verification with 6‑digit codes: send and verify endpoints, signup endpoint, and automatic initial code on new registrations.
    • Verification flow enforces cooldowns, attempt limits, and code expiry; notification includes emphasized code and expiry text.
    • Client SDK: methods to send a code and verify a code.
  • Tests

    • New unit/integration tests covering signup and full verification send/verify flows.

Implement server-side email verification using 6-digit codes with
cooldown enforcement, expiry, and attempt limits. Includes DB migration,
repository with advisory locks, Auth0 signup integration, notification
template, and HTTP SDK methods.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an email verification feature: DB migration and Drizzle schema for email_verification_codes; repository, service, notification template, AuthController signup and verification endpoints; Auth0 helpers; client SDK methods; routes/OpenAPI; tests and snapshots.

Changes

Cohort / File(s) Summary
Database migration & snapshots
apps/api/drizzle/0029_fluffy_drax.sql, apps/api/drizzle/meta/0029_snapshot.json, apps/api/drizzle/meta/_journal.json
Creates email_verification_codes table (UUID id, user_id FK -> userSetting.id ON DELETE cascade), unique index on user_id, idempotent FK creation; adds Drizzle snapshot and journal entry.
Drizzle schema & repository
apps/api/src/auth/model-schemas/email-verification-code/email-verification-code.schema.ts, apps/api/src/auth/model-schemas/index.ts, apps/api/src/auth/repositories/email-verification-code/...
Adds EmailVerificationCodes pgTable and re-export; new EmailVerificationCodeRepository with types and methods upsert, findByUserId, findByUserIdForUpdate, incrementAttempts, and output mapping (ISO timestamps).
Service & business logic
apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts, ...email-verification-code.service.spec.ts
New singleton EmailVerificationCodeService implementing sendCode and verifyCode (6-digit plaintext generation, SHA‑256 storage, 10m expiry, 60s cooldown, max attempts, transactional verification flow) with comprehensive unit tests.
Auth controller, Auth0 service & tests
apps/api/src/auth/controllers/auth/auth.controller.ts, ...auth.controller.spec.ts, apps/api/src/auth/services/auth0/auth0.service.ts, ...auth0.service.spec.ts
Adds signup to AuthController with Auth0 createUser/error mapping; injects EmailVerificationCodeService; adds protected sendVerificationCode and verifyEmailCode endpoints; adds Auth0Service.createUser, markEmailVerified, and AUTH0_DB_CONNECTION; tests updated/added.
Routes, OpenAPI & REST app
apps/api/src/auth/routes/signup/signup.router.ts, .../send-verification-code.router.ts, .../verify-email-code.router.ts, apps/api/src/auth/routes/index.ts, apps/api/src/rest-app.ts
Adds OpenAPI routes /v1/auth/signup, /v1/send-verification-code, /v1/verify-email-code; registers handlers and exports routers.
UserService integration & tests
apps/api/src/user/services/user/user.service.ts, ...user.service.spec.ts, ...user.service.integration.ts
UserService now depends on EmailVerificationCodeService and conditionally sends initial verification code for unverified emails; tests updated to assert behavior and logging on failures.
Notifications template & sender
apps/api/src/notifications/services/notification-templates/email-verification-code-notification.ts, ...notification.spec.ts, apps/notifications/src/.../email-sender.service.ts
Adds emailVerificationCodeNotification producing HTML payload embedding code in <strong> and test; email sender sanitization now allows <strong> tag.
HTTP SDK types & client
packages/http-sdk/src/auth/auth-http.types.ts, packages/http-sdk/src/auth/auth-http.service.ts
Adds SendVerificationCodeResponseSchema type and client methods sendVerificationCode() and verifyEmailCode(code); marks legacy link-based sendVerificationEmail() deprecated.
Tests & snapshots (misc)
various spec files, apps/api/drizzle/meta/0029_snapshot.json
Multiple new/updated tests for services, controllers, notifications, and Auth0 interactions; Drizzle snapshot reflects schema additions.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AuthController
    participant Auth0Service
    participant EmailCodeService
    participant Repo
    participant NotificationService
    participant Database

    Client->>AuthController: POST /v1/auth/signup (email,password)
    AuthController->>Auth0Service: createUser(email,password,AUTH0_DB_CONNECTION)
    Auth0Service->>Auth0Service: call Auth0 Management API
    AuthController-->>Client: 204

    Client->>AuthController: POST /v1/send-verification-code (auth)
    AuthController->>EmailCodeService: sendCode(userId)
    EmailCodeService->>Repo: findByUserId(userId) / check cooldown
    Repo->>Database: SELECT ...
    EmailCodeService->>Repo: upsert(hashedCode,expiresAt,createdAt)
    Repo->>Database: INSERT ... ON CONFLICT UPDATE
    EmailCodeService->>NotificationService: create/send email with plaintext code
    EmailCodeService-->>AuthController: { codeSentAt }
    AuthController-->>Client: 200 { data: { codeSentAt } }

    Client->>AuthController: POST /v1/verify-email-code (code)
    AuthController->>EmailCodeService: verifyCode(userId,code)
    EmailCodeService->>Repo: findByUserIdForUpdate(userId)
    Repo->>Database: SELECT ... FOR UPDATE
    alt code matches and not expired
        EmailCodeService->>Repo: update user (emailVerified=true)
        Repo->>Database: UPDATE users
        EmailCodeService->>Auth0Service: markEmailVerified(auth0Id)
        Auth0Service->>Auth0Service: call Auth0 Management API
    else mismatch
        EmailCodeService->>Repo: incrementAttempts(codeId)
        Repo->>Database: UPDATE attempts
    end
    EmailCodeService-->>AuthController: success or error
    AuthController-->>Client: 204 or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hid a six-digit secret in a burrow snug and neat,

Hashed it safe beneath the roots where codes and timestamps meet,
I sent a little message bold with code and cheer,
Locks and transactions kept it true — hop home, the path is clear,
Verified and cozy now, go nibble on a treat! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately and concisely describes the main change: adding email verification code backend services for the authentication module.
Description check ✅ Passed The PR description provides comprehensive context, including why it was created (CON-197), what was changed (database migration, services, routes, Auth0 methods, tests), and required Auth0 scopes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auth0-verification-backend

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

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 81.73077% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.96%. Comparing base (9ccf71a) to head (3d4996a).
⚠️ Report is 16 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ication-code/email-verification-code.repository.ts 10.52% 15 Missing and 2 partials ⚠️
...s/api/src/auth/controllers/auth/auth.controller.ts 95.45% 1 Missing ⚠️
...erification-code/email-verification-code.schema.ts 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3051      +/-   ##
==========================================
+ Coverage   59.71%   60.96%   +1.25%     
==========================================
  Files        1034     1015      -19     
  Lines       24273    24273              
  Branches     6003     6007       +4     
==========================================
+ Hits        14494    14799     +305     
+ Misses       8528     8266     -262     
+ Partials     1251     1208      -43     
Flag Coverage Δ
api 82.67% <81.73%> (+1.37%) ⬆️
deploy-web 44.35% <ø> (+1.20%) ⬆️
log-collector 85.66% <ø> (+0.48%) ⬆️
notifications 86.06% <ø> (ø)
provider-console 81.48% <ø> (ø)
provider-proxy 85.21% <ø> (ø)
tx-signer 76.26% <ø> (ø)
Files with missing lines Coverage Δ
apps/api/src/auth/services/auth0/auth0.service.ts 100.00% <100.00%> (+25.00%) ⬆️
...rification-code/email-verification-code.service.ts 100.00% <100.00%> (ø)
...-templates/email-verification-code-notification.ts 100.00% <100.00%> (ø)
apps/api/src/rest-app.ts 82.35% <ø> (ø)
apps/api/src/user/services/user/user.service.ts 94.44% <100.00%> (+0.44%) ⬆️
...ions/services/email-sender/email-sender.service.ts 100.00% <ø> (ø)
...s/api/src/auth/controllers/auth/auth.controller.ts 72.72% <95.45%> (+72.72%) ⬆️
...erification-code/email-verification-code.schema.ts 66.66% <66.66%> (ø)
...ication-code/email-verification-code.repository.ts 10.52% <10.52%> (ø)

... and 49 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (10)
apps/api/src/notifications/services/notification-templates/email-verification-code-notification.ts (1)

10-12: Consider extracting the expiry duration to a shared constant.

The "expires in 10 minutes" text is hardcoded here, while the actual expiry (CODE_EXPIRY_MS = 10 * 60 * 1000) is defined in email-verification-code.service.ts. If the expiry changes, these could drift out of sync.

♻️ Optional: Share expiry constant

You could export CODE_EXPIRY_MINUTES from the service or a shared constants file and interpolate it here:

`This code expires in ${CODE_EXPIRY_MINUTES} minutes.`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/notifications/services/notification-templates/email-verification-code-notification.ts`
around lines 10 - 12, The template in email-verification-code-notification.ts
hardcodes "expires in 10 minutes" while the real expiry is defined as
CODE_EXPIRY_MS in email-verification-code.service.ts; to prevent drift, export a
shared expiry constant (e.g., CODE_EXPIRY_MINUTES) from the service or a common
constants module, import that constant into the notification template, and
replace the hardcoded "10" with an interpolated ${CODE_EXPIRY_MINUTES} in the
description string used by the EmailVerificationCodeNotification template.
apps/api/src/auth/routes/signup/signup.router.ts (1)

9-12: Consider enhancing password validation or deferring to Auth0.

The password schema only enforces min(8). Auth0 typically has its own password policy (complexity, special characters, etc.) that will reject weak passwords anyway. However, for better UX, you might want to validate against Auth0's policy client-side or document that Auth0 rules apply.

If you want to match common Auth0 default password policies:

password: z.string()
  .min(8)
  .regex(/[a-z]/, "Must contain lowercase")
  .regex(/[A-Z]/, "Must contain uppercase")
  .regex(/[0-9]/, "Must contain a number")

Alternatively, keep it minimal and let Auth0 return validation errors (current approach is acceptable).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/routes/signup/signup.router.ts` around lines 9 - 12, The
signup password validation currently only enforces min(8) on the
signupInputSchema; either strengthen it to mirror Auth0's common policy by
adding regex checks for lowercase, uppercase, and digit (e.g., add
.regex(/[a-z]/), .regex(/[A-Z]/), .regex(/[0-9]/) to the password z.string()) or
explicitly document in the file that Auth0 enforces stronger password rules and
defer full validation to Auth0; update signupInputSchema accordingly (or add the
explanatory comment next to signupInputSchema) so the intent is clear to future
readers.
apps/api/src/auth/services/auth0/auth0.service.spec.ts (1)

113-127: Consider using vitest-mock-extended for mocking consistency.

The setup function constructs mocks using jest.fn() and casts to unknown as ManagementClient. Per coding guidelines, unit tests should use vitest-mock-extended (mock(), MockProxy<T>) for mocking.

♻️ Suggested refactor using vitest-mock-extended
+import { mock, type MockProxy } from "vitest-mock-extended";
+import type { ManagementClient } from "auth0";

-  function setup(
-    input: {
-      jobs?: { verifyEmail: jest.Mock };
-      users?: { update?: jest.Mock; create?: jest.Mock };
-      usersByEmail?: { getByEmail: jest.Mock };
-    } = {}
-  ) {
-    const mockManagementClient = {
-      ...input
-    } as unknown as ManagementClient;
+  function setup() {
+    const mockManagementClient = mock<ManagementClient>();

     const auth0Service = new Auth0Service(mockManagementClient);

-    return { auth0Service, mockManagementClient };
+    return { auth0Service, managementClient: mockManagementClient };
   }

Then configure mock behavior in individual tests using managementClient.users.create.mockResolvedValue(...).

As per coding guidelines: "Use vitest-mock-extended (mock(), MockProxy<T>) for mocking in unit tests."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/services/auth0/auth0.service.spec.ts` around lines 113 -
127, The setup helper currently builds ad-hoc jest mocks and casts to
ManagementClient; replace this with vitest-mock-extended to create a properly
typed mock: import and use mock<ManagementClient>() (typed as
MockProxy<ManagementClient>) to create mockManagementClient inside setup, remove
the manual jest.Mock shapes, and return { auth0Service, mockManagementClient }
where tests can call typed methods like
mockManagementClient.users.create.mockResolvedValue(...) or
mockManagementClient.jobs.verifyEmail.mockResolvedValue(...); ensure
Auth0Service is instantiated with the MockProxy so TypeScript-aware mocks are
used throughout.
apps/api/src/auth/routes/verify-email-code/verify-email-code.router.ts (1)

11-18: Minor redundancy in validation schema.

The .length(6) check is redundant since the regex /^\d{6}$/ already enforces exactly 6 digits. However, this redundancy is harmless and provides defense-in-depth.

♻️ Optional simplification
 const VerifyEmailCodeRequestSchema = z.object({
   data: z.object({
     code: z
       .string()
-      .length(6)
       .regex(/^\d{6}$/, "Code must be exactly 6 digits")
   })
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/routes/verify-email-code/verify-email-code.router.ts`
around lines 11 - 18, The schema VerifyEmailCodeRequestSchema redundantly
applies .length(6) on the code field while the regex /^\d{6}$/ already enforces
exactly six digits; remove the .length(6) call from the code validator so the
field becomes z.string().regex(/^\d{6}$/, "Code must be exactly 6 digits") to
simplify the validation while preserving the same constraint.
apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts (2)

43-51: Consider asserting the specific error message or status code.

The test only asserts that an error is thrown but doesn't verify it's specifically a 429 rate limit error. This could mask regressions if the error type changes.

💚 More specific assertion
     it("throws 429 when rate limit exceeded", async () => {
       const user = UserSeeder.create({ email: "test@example.com" });
       const { service, emailVerificationCodeRepository, userRepository } = setup();
 
       userRepository.findById.mockResolvedValue(user);
       emailVerificationCodeRepository.countRecentByUserId.mockResolvedValue(5);
 
-      await expect(service.sendCode(user.id)).rejects.toThrow();
+      await expect(service.sendCode(user.id)).rejects.toMatchObject({
+        status: 429,
+        message: "Too many verification code requests. Please try again later."
+      });
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts`
around lines 43 - 51, The test currently just checks that
service.sendCode(user.id) throws, which is too broad; update the assertion to
verify it's specifically a 429 rate-limit error by asserting the thrown error's
type/message/status. Locate the test and replace the generic rejects.toThrow()
with an assertion that inspects the rejected error from service.sendCode (e.g.,
expect(...).rejects.toMatchObject({ status: 429 }) or
expect(...).rejects.toThrowError(/429|rate limit/i) or assert instance of your
RateLimitError class), keeping use of
emailVerificationCodeRepository.countRecentByUserId and the same arrange/act
setup.

53-68: Consider asserting specific error status codes for error cases.

The 404 and 400 error tests only verify that an error is thrown, not the specific status. Verifying status codes ensures the correct HTTP responses reach clients.

💚 More specific assertions
     it("throws 404 when user not found", async () => {
       const { service, userRepository } = setup();
 
       userRepository.findById.mockResolvedValue(undefined);
 
-      await expect(service.sendCode("nonexistent")).rejects.toThrow();
+      await expect(service.sendCode("nonexistent")).rejects.toMatchObject({ status: 404 });
     });
 
     it("throws 400 when user has no email", async () => {
       const user = UserSeeder.create({ email: null });
       const { service, userRepository } = setup();
 
       userRepository.findById.mockResolvedValue(user);
 
-      await expect(service.sendCode(user.id)).rejects.toThrow();
+      await expect(service.sendCode(user.id)).rejects.toMatchObject({ status: 400 });
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts`
around lines 53 - 68, Update the two tests to assert the specific HTTP error
status instead of a generic throw: when userRepository.findById returns
undefined, call service.sendCode("nonexistent") and assert the rejected error
has status 404 (e.g., check error.status or error.code); when
UserSeeder.create({ email: null }) is returned, call service.sendCode(user.id)
and assert the rejected error has status 400. Use the same service.sendCode and
userRepository.findById symbols to locate the tests and replace await
expect(...).rejects.toThrow() with assertions that inspect the thrown error
object for the expected status.
apps/api/src/auth/services/auth0/auth0.service.ts (2)

10-17: Consider returning the created user for downstream use.

The createUser method discards the Auth0 response. If the caller needs the created user's Auth0 ID (e.g., for immediate verification), returning it avoids an extra lookup.

♻️ Optional: return created user data
-  async createUser(input: { email: string; password: string; connection: string }): Promise<void> {
-    await this.managementClient.users.create({
+  async createUser(input: { email: string; password: string; connection: string }): Promise<{ user_id: string }> {
+    const { data } = await this.managementClient.users.create({
       email: input.email,
       password: input.password,
       connection: input.connection,
       verify_email: false
     });
+    return { user_id: data.user_id! };
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/services/auth0/auth0.service.ts` around lines 10 - 17, The
createUser method currently discards the Auth0 response; change it to capture
and return the created user so callers can access the Auth0 ID or other fields.
Update the signature of createUser to return Promise<any> (or a proper Auth0
user type), assign the result of await this.managementClient.users.create({ ...
}) to a const (e.g., createdUser) and return it; ensure callers are updated if
they expect the previous void return. This references createUser and
managementClient.users.create in the existing code.

23-25: Add explicit return type annotation for consistency.

Other methods in this class have explicit return types. Adding Promise<void> maintains consistency.

♻️ Add return type
-  async markEmailVerified(userId: string) {
+  async markEmailVerified(userId: string): Promise<void> {
     await this.managementClient.users.update({ id: userId }, { email_verified: true });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/services/auth0/auth0.service.ts` around lines 23 - 25, The
method markEmailVerified lacks an explicit return type; update its signature to
include Promise<void> (i.e., async markEmailVerified(userId: string):
Promise<void>) to match other methods in the class and maintain consistency,
leaving the body that calls this.managementClient.users.update({ id: userId }, {
email_verified: true }) unchanged.
apps/api/src/auth/controllers/auth/auth.controller.spec.ts (1)

30-38: Test doesn't verify the HTTP status code is preserved.

The test only checks the message but doesn't verify that the 400 status code from Auth0 is preserved in the thrown error.

💚 Enhanced assertion
     it("throws http error when auth0 returns a non-409 error", async () => {
       const { controller, auth0Service } = setup();
 
       auth0Service.createUser.mockRejectedValue(
         new ResponseError(400, JSON.stringify({ message: "PasswordStrengthError: Password is too weak" }), new Headers())
       );
 
-      await expect(controller.signup({ email: "user@example.com", password: "weak" })).rejects.toThrow("PasswordStrengthError: Password is too weak");
+      await expect(controller.signup({ email: "user@example.com", password: "weak" })).rejects.toMatchObject({
+        status: 400,
+        message: "PasswordStrengthError: Password is too weak"
+      });
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/auth/controllers/auth/auth.controller.spec.ts` around lines 30 -
38, The test for controller.signup currently only checks the error message but
not that the original HTTP status code from auth0Service.createUser (a
ResponseError) is preserved; update the test to await the rejected promise and
capture the thrown error from controller.signup, then assert the error is the
expected ResponseError (or has the same shape) and that its status (e.g.,
error.status or error.statusCode) equals 400 while still asserting the message
contains "PasswordStrengthError: Password is too weak"; reference the mocked
auth0Service.createUser, the controller.signup call, and the ResponseError type
when making these assertions.
apps/api/src/auth/repositories/email-verification-code/email-verification-code.repository.ts (1)

9-10: Narrow the repository input type.

Partial<Table["$inferInsert"]> exposes a much wider write surface than this repository actually needs, including DB-managed fields. An explicit input DTO or Pick/Omit-based type will keep inserts harder to misuse and the contract clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/auth/repositories/email-verification-code/email-verification-code.repository.ts`
around lines 9 - 10, The current EmailVerificationCodeInput type uses
Partial<Table["$inferInsert"]>, which exposes DB-managed fields; replace it with
a narrowed DTO using Pick/Omit or an explicit interface that only includes the
fields required for repository inserts (e.g., omit id, created_at, updated_at,
and other DB-managed columns). Update the EmailVerificationCodeInput type
declaration (and any insert/create methods that accept it) so it only accepts
the writable properties the repository actually sets, while keeping
EmailVerificationCodeDbOutput as the selected DB output type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/auth/controllers/auth/auth.controller.ts`:
- Around line 30-40: In the catch block that handles ResponseError in
auth.controller.ts (the code that inspects error instanceof ResponseError and
calls assert), guard the JSON.parse(error.body) call so a non-JSON body cannot
throw: attempt to parse error.body in a try/catch and if parsing fails fall back
to a safe message (e.g., use the raw error.body or a generic message like
"Unknown error from Auth0"); preserve the existing special-case mapping that
turns statusCode 409 into a 422 assertion message, and then call assert(false,
statusCode, message) using the safe message variable so the original parse
failure cannot crash the handler.

In `@apps/api/src/user/services/user/user.service.integration.ts`:
- Around line 347-350: In the test setup where EmailVerificationCodeService is
mocked (the mock<EmailVerificationCodeService> with sendCode), replace the Jest
compatibility global by using Vitest's native API: change
jest.fn().mockResolvedValue(...) to vi.fn().mockResolvedValue(...); ensure any
other occurrences in this test file use vi.fn() instead of jest.fn() so the mock
for sendCode and similar stubs use Vitest directly.

---

Nitpick comments:
In `@apps/api/src/auth/controllers/auth/auth.controller.spec.ts`:
- Around line 30-38: The test for controller.signup currently only checks the
error message but not that the original HTTP status code from
auth0Service.createUser (a ResponseError) is preserved; update the test to await
the rejected promise and capture the thrown error from controller.signup, then
assert the error is the expected ResponseError (or has the same shape) and that
its status (e.g., error.status or error.statusCode) equals 400 while still
asserting the message contains "PasswordStrengthError: Password is too weak";
reference the mocked auth0Service.createUser, the controller.signup call, and
the ResponseError type when making these assertions.

In
`@apps/api/src/auth/repositories/email-verification-code/email-verification-code.repository.ts`:
- Around line 9-10: The current EmailVerificationCodeInput type uses
Partial<Table["$inferInsert"]>, which exposes DB-managed fields; replace it with
a narrowed DTO using Pick/Omit or an explicit interface that only includes the
fields required for repository inserts (e.g., omit id, created_at, updated_at,
and other DB-managed columns). Update the EmailVerificationCodeInput type
declaration (and any insert/create methods that accept it) so it only accepts
the writable properties the repository actually sets, while keeping
EmailVerificationCodeDbOutput as the selected DB output type.

In `@apps/api/src/auth/routes/signup/signup.router.ts`:
- Around line 9-12: The signup password validation currently only enforces
min(8) on the signupInputSchema; either strengthen it to mirror Auth0's common
policy by adding regex checks for lowercase, uppercase, and digit (e.g., add
.regex(/[a-z]/), .regex(/[A-Z]/), .regex(/[0-9]/) to the password z.string()) or
explicitly document in the file that Auth0 enforces stronger password rules and
defer full validation to Auth0; update signupInputSchema accordingly (or add the
explanatory comment next to signupInputSchema) so the intent is clear to future
readers.

In `@apps/api/src/auth/routes/verify-email-code/verify-email-code.router.ts`:
- Around line 11-18: The schema VerifyEmailCodeRequestSchema redundantly applies
.length(6) on the code field while the regex /^\d{6}$/ already enforces exactly
six digits; remove the .length(6) call from the code validator so the field
becomes z.string().regex(/^\d{6}$/, "Code must be exactly 6 digits") to simplify
the validation while preserving the same constraint.

In `@apps/api/src/auth/services/auth0/auth0.service.spec.ts`:
- Around line 113-127: The setup helper currently builds ad-hoc jest mocks and
casts to ManagementClient; replace this with vitest-mock-extended to create a
properly typed mock: import and use mock<ManagementClient>() (typed as
MockProxy<ManagementClient>) to create mockManagementClient inside setup, remove
the manual jest.Mock shapes, and return { auth0Service, mockManagementClient }
where tests can call typed methods like
mockManagementClient.users.create.mockResolvedValue(...) or
mockManagementClient.jobs.verifyEmail.mockResolvedValue(...); ensure
Auth0Service is instantiated with the MockProxy so TypeScript-aware mocks are
used throughout.

In `@apps/api/src/auth/services/auth0/auth0.service.ts`:
- Around line 10-17: The createUser method currently discards the Auth0
response; change it to capture and return the created user so callers can access
the Auth0 ID or other fields. Update the signature of createUser to return
Promise<any> (or a proper Auth0 user type), assign the result of await
this.managementClient.users.create({ ... }) to a const (e.g., createdUser) and
return it; ensure callers are updated if they expect the previous void return.
This references createUser and managementClient.users.create in the existing
code.
- Around line 23-25: The method markEmailVerified lacks an explicit return type;
update its signature to include Promise<void> (i.e., async
markEmailVerified(userId: string): Promise<void>) to match other methods in the
class and maintain consistency, leaving the body that calls
this.managementClient.users.update({ id: userId }, { email_verified: true })
unchanged.

In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts`:
- Around line 43-51: The test currently just checks that
service.sendCode(user.id) throws, which is too broad; update the assertion to
verify it's specifically a 429 rate-limit error by asserting the thrown error's
type/message/status. Locate the test and replace the generic rejects.toThrow()
with an assertion that inspects the rejected error from service.sendCode (e.g.,
expect(...).rejects.toMatchObject({ status: 429 }) or
expect(...).rejects.toThrowError(/429|rate limit/i) or assert instance of your
RateLimitError class), keeping use of
emailVerificationCodeRepository.countRecentByUserId and the same arrange/act
setup.
- Around line 53-68: Update the two tests to assert the specific HTTP error
status instead of a generic throw: when userRepository.findById returns
undefined, call service.sendCode("nonexistent") and assert the rejected error
has status 404 (e.g., check error.status or error.code); when
UserSeeder.create({ email: null }) is returned, call service.sendCode(user.id)
and assert the rejected error has status 400. Use the same service.sendCode and
userRepository.findById symbols to locate the tests and replace await
expect(...).rejects.toThrow() with assertions that inspect the thrown error
object for the expected status.

In
`@apps/api/src/notifications/services/notification-templates/email-verification-code-notification.ts`:
- Around line 10-12: The template in email-verification-code-notification.ts
hardcodes "expires in 10 minutes" while the real expiry is defined as
CODE_EXPIRY_MS in email-verification-code.service.ts; to prevent drift, export a
shared expiry constant (e.g., CODE_EXPIRY_MINUTES) from the service or a common
constants module, import that constant into the notification template, and
replace the hardcoded "10" with an interpolated ${CODE_EXPIRY_MINUTES} in the
description string used by the EmailVerificationCodeNotification template.
🪄 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

Run ID: 102172ac-ceac-4fbf-a247-c6f8aa6c9946

📥 Commits

Reviewing files that changed from the base of the PR and between 9ccf71a and a699f54.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (25)
  • apps/api/drizzle/0029_fluffy_drax.sql
  • apps/api/drizzle/meta/0029_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/auth/controllers/auth/auth.controller.spec.ts
  • apps/api/src/auth/controllers/auth/auth.controller.ts
  • apps/api/src/auth/model-schemas/email-verification-code/email-verification-code.schema.ts
  • apps/api/src/auth/model-schemas/index.ts
  • apps/api/src/auth/repositories/email-verification-code/email-verification-code.repository.ts
  • apps/api/src/auth/routes/index.ts
  • apps/api/src/auth/routes/send-verification-code/send-verification-code.router.ts
  • apps/api/src/auth/routes/signup/signup.router.ts
  • apps/api/src/auth/routes/verify-email-code/verify-email-code.router.ts
  • apps/api/src/auth/services/auth0/auth0.service.spec.ts
  • apps/api/src/auth/services/auth0/auth0.service.ts
  • apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts
  • apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts
  • apps/api/src/notifications/services/notification-templates/email-verification-code-notification.spec.ts
  • apps/api/src/notifications/services/notification-templates/email-verification-code-notification.ts
  • apps/api/src/rest-app.ts
  • apps/api/src/user/services/user/user.service.integration.ts
  • apps/api/src/user/services/user/user.service.spec.ts
  • apps/api/src/user/services/user/user.service.ts
  • apps/notifications/src/modules/notifications/services/email-sender/email-sender.service.ts
  • packages/http-sdk/src/auth/auth-http.service.ts
  • packages/http-sdk/src/auth/auth-http.types.ts

Comment thread apps/api/src/auth/controllers/auth/auth.controller.ts
Comment thread apps/api/src/user/services/user/user.service.integration.ts
Replace http-assert with http-errors for error handling consistency.
Add safe JSON parsing for Auth0 error bodies. Fix UserSeeder import
to use existing createUser function and replace jest.fn with vi.fn.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts (1)

43-68: Tighten negative-path assertions to check expected error shape.

A few tests only use rejects.toThrow(), which can pass on unrelated failures. Since the test names specify 429/404/400 behavior, assert status/message explicitly to prevent false positives.

Example pattern
- await expect(service.sendCode(user.id)).rejects.toThrow();
+ await expect(service.sendCode(user.id)).rejects.toMatchObject({
+   status: 429
+ });

Also applies to: 99-109, 138-146

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts`
around lines 43 - 68, The tests for email-verification-code.service are using
generic rejects.toThrow() and should assert the specific error shape and status
to avoid false positives; update the negative tests around service.sendCode to
capture the rejected error from sendCode (when userRepository.findById returns
undefined, when user.email is null, and when
emailVerificationCodeRepository.countRecentByUserId returns 5) and assert the
thrown error contains the expected HTTP status codes (429, 404, 400) and/or
message fields rather than a generic toThrow; locate these assertions near
service.sendCode, userRepository.findById and
emailVerificationCodeRepository.countRecentByUserId in the spec and replace the
loose rejects.toThrow() with explicit checks on error.status or error.message
(or toMatchObject) matching the test names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts`:
- Line 15: The test file imports EmailVerificationCodeService via a relative
path; update the import to use the TypeScript path alias for backend sources
(use the `@src/`* alias instead of "./email-verification-code.service") so the
spec imports EmailVerificationCodeService from the aliased module location
matching the project's backend alias rules; ensure the import string uses
`@src/`.../email-verification-code.service and keep the imported symbol
EmailVerificationCodeService unchanged.

---

Nitpick comments:
In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts`:
- Around line 43-68: The tests for email-verification-code.service are using
generic rejects.toThrow() and should assert the specific error shape and status
to avoid false positives; update the negative tests around service.sendCode to
capture the rejected error from sendCode (when userRepository.findById returns
undefined, when user.email is null, and when
emailVerificationCodeRepository.countRecentByUserId returns 5) and assert the
thrown error contains the expected HTTP status codes (429, 404, 400) and/or
message fields rather than a generic toThrow; locate these assertions near
service.sendCode, userRepository.findById and
emailVerificationCodeRepository.countRecentByUserId in the spec and replace the
loose rejects.toThrow() with explicit checks on error.status or error.message
(or toMatchObject) matching the test names.
🪄 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

Run ID: 6455759f-cda3-47af-979b-c7d41a25085f

📥 Commits

Reviewing files that changed from the base of the PR and between a699f54 and f4a31ed.

📒 Files selected for processing (5)
  • apps/api/src/auth/controllers/auth/auth.controller.spec.ts
  • apps/api/src/auth/controllers/auth/auth.controller.ts
  • apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts
  • apps/api/src/user/services/user/user.service.integration.ts
  • apps/api/src/user/services/user/user.service.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/api/src/auth/controllers/auth/auth.controller.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/api/src/user/services/user/user.service.integration.ts
  • apps/api/src/user/services/user/user.service.spec.ts
  • apps/api/src/auth/controllers/auth/auth.controller.ts

- Replace jest.fn/jest.Mock with vi.fn/Mock from vitest in auth0.service.spec
- Remove redundant .length(6) validation (regex already enforces 6 digits)
- Parallelize independent DB reads in sendCode via Promise.all
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts (1)

67-90: Consider invalidating the verification code after successful verification.

After the code is successfully verified (line 87), the code record remains active in the database until it expires. While not a security issue (the user is already verified), this means:

  1. Unnecessary DB lookups on subsequent calls until expiration.
  2. The code could theoretically be "verified" multiple times (idempotent but wasteful).

Consider deleting or marking the code as used after successful verification.

♻️ Suggested fix
     await this.userRepository.updateById(userInternalId, { emailVerified: true });
+    await this.emailVerificationCodeRepository.deleteById(record.id);

     return user.userId;

This would require adding a deleteById method to the repository if it doesn't exist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts`
around lines 67 - 90, The verifyCodeInTransaction method leaves the verification
record active after successful verification; update it to invalidate the record
(e.g., call emailVerificationCodeRepository.deleteById(record.id) or an
equivalent mark-as-used method) immediately after setting user.emailVerified (or
before returning) so the code cannot be reused and DB lookups stop; add or use a
deleteById/markUsed method on emailVerificationCodeRepository if missing and
ensure this delete runs inside the same transaction/WithTransaction context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts`:
- Around line 59-65: The verifyCode flow can leave local DB and Auth0
inconsistent because auth0Service.markEmailVerified(auth0UserId) is called
outside the transaction; wrap the call in a try-catch inside verifyCode
(referencing verifyCode and verifyCodeInTransaction), on failure log a distinct
event (e.g., "EMAIL_VERIFIED_MARK_AUTH0_FAILED") with auth0UserId,
userInternalId and the error via this.logger.error, and implement retry
semantics (e.g., limited immediate retries with backoff) or enqueue a background
reconciliation job/message for later retry; ensure the controller still surfaces
a clear error when retries/external reconciliation are scheduled and document
that downstream flows rely on local emailVerified state.

---

Nitpick comments:
In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts`:
- Around line 67-90: The verifyCodeInTransaction method leaves the verification
record active after successful verification; update it to invalidate the record
(e.g., call emailVerificationCodeRepository.deleteById(record.id) or an
equivalent mark-as-used method) immediately after setting user.emailVerified (or
before returning) so the code cannot be reused and DB lookups stop; add or use a
deleteById/markUsed method on emailVerificationCodeRepository if missing and
ensure this delete runs inside the same transaction/WithTransaction context.
🪄 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

Run ID: 9a529fde-fcf6-4214-9b76-f11dc46ea15c

📥 Commits

Reviewing files that changed from the base of the PR and between f4a31ed and 3cd8f4a.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • apps/api/src/auth/routes/verify-email-code/verify-email-code.router.ts
  • apps/api/src/auth/services/auth0/auth0.service.spec.ts
  • apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/auth/services/auth0/auth0.service.spec.ts
  • apps/api/src/auth/routes/verify-email-code/verify-email-code.router.ts

- Use @src/* path alias instead of relative import in spec file
- Add try-catch with structured error logging around Auth0 markEmailVerified
@stalniy
Copy link
Copy Markdown
Contributor

stalniy commented Apr 13, 2026

@baktun14 could you please explain why you decided to use SELECT FOR UPDATE instead of unique or compound unique index?

I also don’t understand why there are multiple rows per user in the table. How do you plan to use it?

…tion codes

Addresses review feedback: unique index on userId enforces one code per
user at the DB level, upsert eliminates send/send and send/verify race
conditions, cooldown derived from expiresAt replaces multi-row counting,
and direct hash comparison replaces unnecessary timingSafeEqual.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts (1)

36-52: ⚠️ Potential issue | 🟠 Major

Make the cooldown check atomic with the write.

Line 36 reads the current state, and Line 45 then performs an unconditional upsert. Two concurrent sendCode() calls can both pass the cooldown check, generate different codes, and both send emails; the second upsert wins, so one of those emails is immediately invalid. This needs to be enforced in one locked/atomic DB operation instead of a read-then-write split.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts`
around lines 36 - 52, The cooldown check is non-atomic: sendCode currently uses
emailVerificationCodeRepository.findByUserId to read then
emailVerificationCodeRepository.upsert to write, allowing races where two
requests pass the check and both send codes. Replace the read-then-upsert with a
single atomic DB operation (transaction or conditional upsert) inside the
emailVerificationCodeRepository (e.g., add a method like
createOrUpdateIfNotInCooldown or upsertWithCooldownCheck) that checks
existing.expiresAt against Date.now() + CODE_EXPIRY_MS - COOLDOWN_MS and either
returns an error (429) when within cooldown or writes the new hashed
code/expiresAt and returns the persisted record; then call that new atomic
repository method from the service and only send notification after the
repository confirms the upsert succeeded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts`:
- Around line 45-52: The code currently persists the verification code via
emailVerificationCodeRepository.upsert before calling
notificationService.createNotification, which can leave a valid code active if
notification queuing fails; modify the flow so that notification queuing
succeeds before treating the code as cooldown-producing: either move the upsert
until after createNotification succeeds, or wrap createNotification in a
try/catch and on error remove/expire the persisted record (e.g. call
emailVerificationCodeRepository.delete or update expiresAt to now for the record
returned by upsert); ensure you reference
emailVerificationCodeRepository.upsert, notificationService.createNotification,
and emailVerificationCodeNotification when implementing the rollback or the
reordered flow.

---

Duplicate comments:
In
`@apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts`:
- Around line 36-52: The cooldown check is non-atomic: sendCode currently uses
emailVerificationCodeRepository.findByUserId to read then
emailVerificationCodeRepository.upsert to write, allowing races where two
requests pass the check and both send codes. Replace the read-then-upsert with a
single atomic DB operation (transaction or conditional upsert) inside the
emailVerificationCodeRepository (e.g., add a method like
createOrUpdateIfNotInCooldown or upsertWithCooldownCheck) that checks
existing.expiresAt against Date.now() + CODE_EXPIRY_MS - COOLDOWN_MS and either
returns an error (429) when within cooldown or writes the new hashed
code/expiresAt and returns the persisted record; then call that new atomic
repository method from the service and only send notification after the
repository confirms the upsert succeeded.
🪄 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

Run ID: b5b09353-1da9-43d4-851d-f370d2d0cf17

📥 Commits

Reviewing files that changed from the base of the PR and between 248c38a and 795c74f.

📒 Files selected for processing (6)
  • apps/api/drizzle/0029_fluffy_drax.sql
  • apps/api/drizzle/meta/0029_snapshot.json
  • apps/api/src/auth/model-schemas/email-verification-code/email-verification-code.schema.ts
  • apps/api/src/auth/repositories/email-verification-code/email-verification-code.repository.ts
  • apps/api/src/auth/services/email-verification-code/email-verification-code.service.spec.ts
  • apps/api/src/auth/services/email-verification-code/email-verification-code.service.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/api/drizzle/meta/0029_snapshot.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/api/src/auth/model-schemas/email-verification-code/email-verification-code.schema.ts
  • apps/api/drizzle/0029_fluffy_drax.sql
  • apps/api/src/auth/repositories/email-verification-code/email-verification-code.repository.ts

@baktun14
Copy link
Copy Markdown
Contributor Author

@baktun14 could you please explain why you decided to use SELECT FOR UPDATE instead of unique or compound unique index?

I also don’t understand why there are multiple rows per user in the table. How do you plan to use it?

No particular good reason, oversight on my part.

If createNotification throws after upserting the code, the record is now
deleted so the user isn't blocked by cooldown without ever receiving the
code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants