Skip to content

Conversation

@richiemcilroy
Copy link
Member

@richiemcilroy richiemcilroy commented Aug 4, 2025

This pull request implements a new OTP-based email verification system to enhance user authentication. The changes include backend logic for generating and validating OTPs, updates to the authentication flow, and a new frontend UI for OTP verification.

Demo: https://Cap.link/9s2fn1n2ewrpnyy

Summary by CodeRabbit

  • New Features

    • Replaced magic-link login with OTP-based email sign-in. Users receive a 6-digit code and verify on a new Verify Code page.
    • 30-second cooldown on sending/resending codes, with user feedback and preserved next URL during redirects.
  • UX Improvements

    • Six-field OTP input with paste and backspace navigation, autofocus, loading states, and subtle animations.
    • Clear error/success toasts and links to terms and privacy.
  • Chores

    • Backend and database updated to support OTP verification emails.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 4, 2025

Walkthrough

Introduces OTP-based email authentication. Login form adds client-side rate limiting and redirects to a new verify-OTP page. New VerifyOTP form/page handle 6-digit code entry, verification, and resend with cooldown. Auth options switch EmailProvider to OTP generation/delivery. Adds OTP email template, migration snapshot/journal entry, and refactors/formatting in adapter/schema.

Changes

Cohort / File(s) Summary of changes
Web login flow update
apps/web/app/(org)/login/form.tsx
Adds 30s client-side rate limit for email sign-in; on success, redirects to /verify-otp with query params; updates error handling and routing.
Verify OTP UI and page
apps/web/app/(org)/verify-otp/form.tsx, apps/web/app/(org)/verify-otp/page.tsx
New OTP verification form (6 inputs, paste/backspace support, resend with 30s cooldown) and server page that guards session, reads query params, and renders form.
Auth options: OTP provider
packages/database/auth/auth-options.tsx
Replaces magic-link with OTP: generates 6-digit codes, updates EmailProvider sendVerificationRequest signature, dev logging vs production email, preserves existing sign-in events/callbacks structure.
Email template: OTP
packages/database/emails/otp-email.tsx
New React email component to send OTP code with instructions and preview text.
Migrations metadata
packages/database/migrations/meta/0007_snapshot.json, packages/database/migrations/meta/_journal.json
Adds snapshot for version 5 schema including otp_codes and related tables; updates journal with new entry.
Drizzle adapter refactor
packages/database/auth/drizzle-adapter.ts
Formatting; updates createVerificationToken to update-or-insert then select; removes a console log; no signature changes.
Schema formatting
packages/database/schema.ts
Formatting/whitespace-only changes; no schema or API changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant L as Login Form (Client)
  participant NA as NextAuth EmailProvider
  participant E as Email Service
  participant V as Verify OTP Page (Client)
  participant S as Server/Auth

  U->>L: Enter email, submit
  L->>L: Enforce 30s client cooldown
  L->>NA: signIn("email", { email })
  NA->>NA: Generate 6-digit OTP
  alt Production
    NA->>E: Send OTP email
    E-->>U: Deliver OTP code
  else Development
    NA-->>NA: Log OTP to console
  end
  NA-->>L: Response
  L->>V: Redirect /verify-otp?email=...&lastSent=ts

  U->>V: Enter 6-digit code
  V->>S: verifyOTPAction(email, code)
  alt Valid OTP
    S-->>V: OK
    V->>NA: signIn("email-otp", { email, code })
    NA-->>V: Session established
    V->>U: Redirect to next or /dashboard
  else Invalid/expired OTP
    S-->>V: Error
    V-->>U: Show error, reset inputs
  end
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant V as Verify OTP Page (Client)
  participant NA as NextAuth EmailProvider
  participant E as Email Service

  U->>V: Click "Resend code"
  V->>V: Check 30s cooldown
  alt Cooldown active
    V-->>U: Toast: please wait
  else Cooldown passed
    V->>NA: signIn("email", { email })
    NA->>NA: Generate new 6-digit OTP
    NA->>E: Send OTP email (or log in dev)
    E-->>U: New OTP delivered
    V-->>U: Toast: code sent
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit taps six digits—hop, hop, hop—
Codes like carrots in a secret crop.
A blink, a link, a gentle OTP,
Through burrows of auth to identity.
Resend? Wait—thirty heartbeats more—
Then bound to the dashboard, ears galore.
Verified, we’re out the door! 🥕✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch otp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
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: 12

🧹 Nitpick comments (5)
packages/database/schema.ts (1)

129-145: Well-designed OTP table schema with good security practices.

The table structure follows established patterns in the codebase and includes important security features:

  • Proper indexing for query performance
  • Attempt tracking to prevent brute force attacks
  • Expiration handling for security
  • Consistent use of nanoId and timestamp patterns

Consider implementing a cleanup job to periodically remove expired OTP codes to prevent table bloat.

apps/web/app/(org)/verify-otp/page.tsx (1)

10-32: Well-structured page component with proper session handling.

The component correctly handles authentication states and validates required parameters. The redirect logic ensures authenticated users don't unnecessarily see the OTP form.

Consider replacing the basic "Loading..." text with a more polished loading component for better UX.

-      <Suspense fallback={<div>Loading...</div>}>
+      <Suspense fallback={<div className="text-center text-gray-10">Loading verification form...</div>}>
packages/database/emails/otp-email.tsx (2)

16-22: Consider removing default empty string values for required props

Props email and code should be required without defaults, as empty values would result in a meaningless email. Consider using TypeScript's required properties instead.

-export function OTPEmail({
-  email = "",
-  code = "",
-}: {
+export function OTPEmail({
+  email,
+  code,
+}: {
   email: string;
   code: string;
 }) {

26-26: Security consideration: OTP code visible in email preview

The OTP code is included in the email preview text, which may be visible in email notifications on locked devices. Consider showing only a partial code or generic text for better security.

-      <Preview>Your Cap Verification Code: {code}</Preview>
+      <Preview>Your Cap Verification Code</Preview>
packages/database/migrations/meta/0007_snapshot.json (1)

789-792: Consider adding a compound index for better query performance

The validateOTP function queries by both identifier and code together. A compound index would improve performance.

Consider adding a compound index on (identifier, code) in your schema definition to optimize the OTP validation queries. This would be more efficient than using separate indexes.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b402cee and 2e3d331.

📒 Files selected for processing (10)
  • apps/web/app/(org)/login/form.tsx (1 hunks)
  • apps/web/app/(org)/verify-otp/actions.ts (1 hunks)
  • apps/web/app/(org)/verify-otp/form.tsx (1 hunks)
  • apps/web/app/(org)/verify-otp/page.tsx (1 hunks)
  • packages/database/auth/auth-options.tsx (3 hunks)
  • packages/database/auth/otp.ts (1 hunks)
  • packages/database/emails/otp-email.tsx (1 hunks)
  • packages/database/migrations/meta/0007_snapshot.json (1 hunks)
  • packages/database/migrations/meta/_journal.json (1 hunks)
  • packages/database/schema.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
apps/web/app/(org)/verify-otp/actions.ts (1)
packages/database/auth/otp.ts (1)
  • validateOTP (45-118)
apps/web/app/(org)/verify-otp/page.tsx (2)
packages/database/auth/session.ts (1)
  • getSession (9-13)
apps/web/app/(org)/verify-otp/form.tsx (1)
  • VerifyOTPForm (13-192)
packages/database/auth/otp.ts (4)
packages/database/index.ts (1)
  • db (28-33)
packages/database/schema.ts (1)
  • otpCodes (129-145)
apps/web/tools/compress-images.js (1)
  • crypto (17-17)
packages/database/helpers.ts (1)
  • nanoId (6-9)
packages/database/schema.ts (1)
packages/database/helpers.ts (1)
  • nanoId (6-9)
🪛 GitHub Actions: Validate Migrations
packages/database/migrations/meta/_journal.json

[error] 2-2: Migration journal version cannot be changed (was: 5, now: unknown).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
🔇 Additional comments (6)
apps/web/app/(org)/login/form.tsx (1)

238-242: LGTM! Clean integration with the OTP verification flow.

The redirect logic correctly constructs query parameters and navigates to the OTP verification page after successful email submission. The conditional inclusion of the next parameter ensures proper routing continuation.

apps/web/app/(org)/verify-otp/actions.ts (1)

5-22: Excellent implementation with proper security practices.

The server action correctly validates inputs, handles errors appropriately, and maintains security by:

  • Validating required parameters upfront
  • Using generic error messages to prevent information leakage
  • Proper error logging for debugging
  • Consistent response format for client handling
packages/database/emails/otp-email.tsx (1)

56-56: Hardcoded expiration time should match backend configuration

The "10 minutes" expiration mentioned here should be synchronized with the actual OTP_EXPIRY_MINUTES constant in the backend to avoid confusion.

Consider making this value configurable or passed as a prop to ensure consistency with the backend configuration.

packages/database/auth/otp.ts (2)

27-27: Security: Use cryptographically secure random number generation

While crypto.randomInt is cryptographically secure, the current implementation generates predictable 6-digit codes (100000-999999). Consider using a more secure approach.

-  const code = crypto.randomInt(100000, 1000000).toString();
+  // Generate a truly random 6-digit code including leading zeros
+  const code = crypto.randomInt(0, 1000000).toString().padStart(6, '0');

Likely an incorrect or invalid review comment.


121-122: Incorrect comparison in cleanup function

The comparison in the WHERE clause is backwards - it should check where expires is less than current time.

-  await db().delete(otpCodes).where(gt(new Date(), otpCodes.expires));
+  await db().delete(otpCodes).where(lt(otpCodes.expires, new Date()));

Note: You'll need to import lt from drizzle-orm:

-import { eq, and, gt, gte, sql } from "drizzle-orm";
+import { eq, and, gt, gte, lt, sql } from "drizzle-orm";

Likely an incorrect or invalid review comment.

packages/database/migrations/meta/0007_snapshot.json (1)

770-868: LGTM! Well-structured OTP table definition

The otp_codes table is well-designed with appropriate:

  • Indexes on identifier, code, and expires for query performance
  • Proper data types and constraints
  • Automatic timestamp management

Comment on lines 70 to 74
await signIn("otp", {
email,
redirect: true,
callbackUrl: next || "/dashboard",
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential security issue: Missing error handling for signIn

The signIn call after successful OTP verification doesn't handle potential failures, which could leave users in a confused state.

       if (result.success) {
-        await signIn("otp", {
+        const signInResult = await signIn("otp", {
           email,
           redirect: true,
           callbackUrl: next || "/dashboard",
         });
+        
+        if (signInResult?.error) {
+          toast.error("Authentication failed. Please try again.");
+          setCode(["", "", "", "", "", ""]);
+          inputRefs.current[0]?.focus();
+        }
       } else {
🤖 Prompt for AI Agents
In apps/web/app/(org)/verify-otp/form.tsx around lines 70 to 74, the signIn call
lacks error handling, which may cause user confusion if sign-in fails. Modify
the code to capture the result of the signIn call, check for errors or
unsuccessful sign-in, and handle these cases appropriately by showing an error
message or preventing redirection. This ensures users receive clear feedback on
sign-in failures.

Comment on lines 134 to 137
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={1}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add proper HTML attributes for better accessibility and UX

The input fields are missing important attributes for accessibility and mobile UX.

             type="text"
             inputMode="numeric"
             pattern="[0-9]*"
+            autoComplete="one-time-code"
+            aria-label={`Digit ${index + 1} of 6`}
             maxLength={1}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={1}
type="text"
inputMode="numeric"
pattern="[0-9]*"
autoComplete="one-time-code"
aria-label={`Digit ${index + 1} of 6`}
maxLength={1}
🤖 Prompt for AI Agents
In apps/web/app/(org)/verify-otp/form.tsx around lines 134 to 137, the input
fields lack important HTML attributes that improve accessibility and mobile user
experience. Add the attribute aria-label or aria-labelledby to describe the
input purpose for screen readers, and include autoComplete="one-time-code" to
enable better OTP autofill on supported devices. Also, consider adding
inputMode="numeric" and pattern="[0-9]*" if not already present to optimize
mobile keyboard display.

Comment on lines 141 to 145
onPaste={(e) => {
e.preventDefault();
const pastedData = e.clipboardData.getData("text").replace(/\D/g, "");
handleChange(0, pastedData);
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Redundant paste handler on first input

The paste event handler on line 141-145 is redundant because handleChange already handles pasted content. This could cause issues with the paste functionality.

-            onPaste={(e) => {
-              e.preventDefault();
-              const pastedData = e.clipboardData.getData("text").replace(/\D/g, "");
-              handleChange(0, pastedData);
-            }}
+            onPaste={(e) => {
+              // Only prevent default on non-first inputs to avoid double handling
+              if (index !== 0) {
+                e.preventDefault();
+                const pastedData = e.clipboardData.getData("text").replace(/\D/g, "");
+                handleChange(index, pastedData);
+              }
+            }}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/app/(org)/verify-otp/form.tsx around lines 141 to 145, remove the
onPaste event handler from the first input element because handleChange already
processes pasted content. This will prevent redundant handling and potential
conflicts with paste functionality.

} else {
console.log({ identifier, url });
const email = LoginLink({ url, email: identifier });
console.log({ identifier, otpCode });
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove sensitive information from logs

Logging the OTP code in production (even with RESEND_API_KEY check) poses a security risk.

-              console.log({ identifier, otpCode });
+              console.log({ identifier, otpSent: true });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log({ identifier, otpCode });
console.log({ identifier, otpSent: true });
🤖 Prompt for AI Agents
In packages/database/auth/auth-options.tsx at line 123, remove the console.log
statement that outputs the OTP code and identifier to avoid logging sensitive
information. Instead, either remove the log entirely or log only non-sensitive
information that does not include the OTP code to maintain security.

Comment on lines 11 to 43
export async function generateOTP(identifier: string): Promise<string> {
const recentOTP = await db()
.select()
.from(otpCodes)
.where(
and(
eq(otpCodes.identifier, identifier),
gte(otpCodes.createdAt, new Date(Date.now() - MIN_TIME_BETWEEN_REQUESTS_MS))
)
)
.limit(1);

if (recentOTP.length > 0) {
throw new Error("Please wait before requesting a new code");
}

const code = crypto.randomInt(100000, 1000000).toString();

const expires = new Date();
expires.setMinutes(expires.getMinutes() + OTP_EXPIRY_MINUTES);

await db().delete(otpCodes).where(eq(otpCodes.identifier, identifier));

await db().insert(otpCodes).values({
id: nanoId(),
identifier,
code,
expires,
attempts: 0,
});

return code;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Add database transaction to prevent race conditions

The OTP generation process involves checking for recent OTPs, deleting existing ones, and inserting new ones. These operations should be wrapped in a transaction to prevent race conditions.

Consider wrapping the database operations in a transaction:

export async function generateOTP(identifier: string): Promise<string> {
  return await db().transaction(async (tx) => {
    const recentOTP = await tx
      .select()
      .from(otpCodes)
      .where(
        and(
          eq(otpCodes.identifier, identifier),
          gte(otpCodes.createdAt, new Date(Date.now() - MIN_TIME_BETWEEN_REQUESTS_MS))
        )
      )
      .limit(1);

    if (recentOTP.length > 0) {
      throw new Error("Please wait before requesting a new code");
    }

    const code = crypto.randomInt(100000, 1000000).toString();
    
    const expires = new Date();
    expires.setMinutes(expires.getMinutes() + OTP_EXPIRY_MINUTES);

    await tx.delete(otpCodes).where(eq(otpCodes.identifier, identifier));

    await tx.insert(otpCodes).values({
      id: nanoId(),
      identifier,
      code,
      expires,
      attempts: 0,
    });

    return code;
  });
}
🤖 Prompt for AI Agents
In packages/database/auth/otp.ts around lines 11 to 43, the OTP generation logic
performs multiple database operations that should be atomic to avoid race
conditions. Wrap all database calls (select, delete, insert) inside a single
transaction using db().transaction and replace db() calls with the transaction
object (e.g., tx) to ensure these operations execute atomically and
consistently.

Comment on lines 49 to 51
if (!code.match(/^\d{6}$/)) {
return { valid: false, error: "Invalid code format" };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential timing attack vulnerability in code validation

The regex validation returns early with a different error, which could allow timing attacks to determine valid code formats.

Consider validating the format after database lookup to maintain consistent timing:

-  if (!code.match(/^\d{6}$/)) {
-    return { valid: false, error: "Invalid code format" };
-  }
   const otpRecord = await db()
     .select()
     .from(otpCodes)
     .where(
       and(
         eq(otpCodes.identifier, identifier),
         eq(otpCodes.code, code),
         gt(otpCodes.expires, new Date())
       )
     )
     .limit(1);
+
+  // Validate format after DB lookup to prevent timing attacks
+  if (!code.match(/^\d{6}$/)) {
+    return { valid: false, error: "Invalid code" };
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!code.match(/^\d{6}$/)) {
return { valid: false, error: "Invalid code format" };
}
const otpRecord = await db()
.select()
.from(otpCodes)
.where(
and(
eq(otpCodes.identifier, identifier),
eq(otpCodes.code, code),
gt(otpCodes.expires, new Date())
)
)
.limit(1);
// Validate format after DB lookup to prevent timing attacks
if (!code.match(/^\d{6}$/)) {
return { valid: false, error: "Invalid code" };
}
🤖 Prompt for AI Agents
In packages/database/auth/otp.ts around lines 49 to 51, the current code returns
early on regex validation failure, causing timing differences that could be
exploited in timing attacks. To fix this, remove the early return on regex
mismatch and instead perform the database lookup first, then validate the code
format afterward to ensure consistent response timing regardless of input
validity.

Comment on lines 54 to 60
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Critical: Address the pipeline failure for migration version mismatch.

The pipeline is failing because the migration journal version appears to have changed unexpectedly. Additionally, the timestamp 1754314124918 corresponds to a date far in the future (~2025), which suggests either a system clock issue or test data being used.

Please run the following script to verify the migration state and timestamp:


🏁 Script executed:

#!/bin/bash
# Description: Check migration journal consistency and validate timestamp

# Check if migration files exist for the new entry
fd "0007_cheerful_rocket_raccoon" packages/database/migrations/

# Convert timestamp to human-readable date
echo "Timestamp 1754314124918 converts to:"
python -c "import datetime; print(datetime.datetime.fromtimestamp(1754314124918/1000))"

# Check current system time for comparison
echo "Current system time:"
date

Length of output: 350


Critical: Missing migration script for new journal entry

The journal entry in packages/database/migrations/meta/_journal.json for version 5 (tag "0007_cheerful_rocket_raccoon") points to a migration that doesn’t exist—this is causing the pipeline to fail. The timestamp (2025-08-04 13:28:44.918 UTC) is valid relative to the current system time and does not indicate a clock issue.

• Add the corresponding migration file (e.g. 0007_cheerful_rocket_raccoon.sql or .ts) under packages/database/migrations/.
• Ensure the filename, tag, and version all match exactly.
• Re-run the migration pipeline to confirm consistency.

🤖 Prompt for AI Agents
In packages/database/migrations/meta/_journal.json at lines 54 to 60, the
journal entry for version 5 with tag "0007_cheerful_rocket_raccoon" references a
migration that does not exist, causing pipeline failures. To fix this, create
the missing migration script file named exactly
"0007_cheerful_rocket_raccoon.sql" or ".ts" in the packages/database/migrations/
directory, ensuring the filename, tag, and version match the journal entry.
After adding the file, rerun the migration pipeline to verify the issue is
resolved.

Copy link
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: 3

🧹 Nitpick comments (6)
packages/database/schema.ts (2)

450-452: Fix Biome error: remove empty object pattern in relations callback

The empty object pattern ({ }) => ({}) triggers Biome's noEmptyPattern error. It’s unnecessary here.

-export const verificationTokensRelations = relations(
-  verificationTokens,
-  ({ }) => ({
-    // No relations defined
-  })
-);
+export const verificationTokensRelations = relations(
+  verificationTokens,
+  () => ({
+    // No relations defined
+  })
+);

357-367: Encrypted text customType only sets SQL type; ensure actual encryption happens upstream

The encryptedText customType maps to TEXT, which does not enforce encryption. If actual encryption/decryption is performed in a different layer, consider documenting it here to avoid a false sense of security. If it’s not implemented, add app-layer crypto or KMS integration.

packages/database/auth/drizzle-adapter.ts (2)

136-137: Misleading error message: this refers to a session, not a user

Throwing “User not found” on a missing session is confusing.

-      if (!row) throw new Error("User not found");
+      if (!row) throw new Error("Session not found");

236-252: Strengthen verification token usage with identifier and expiry checks

To reduce accidental cross-identifier matches and ensure expiration is enforced at the DB layer, include identifier and an expires > now() predicate in both selection and deletion.

-      const rows = await db
-        .select()
-        .from(verificationTokens)
-        .where(eq(verificationTokens.token, token))
-        .limit(1);
+      const rows = await db
+        .select()
+        .from(verificationTokens)
+        .where(
+          and(
+            eq(verificationTokens.token, token),
+            eq(verificationTokens.identifier, identifier),
+            // ensure token not expired
+            gt(verificationTokens.expires, new Date())
+          )
+        )
+        .limit(1);
       const row = rows[0];
       if (!row) return null;
       await db
         .delete(verificationTokens)
         .where(
           and(
             eq(verificationTokens.token, token),
             eq(verificationTokens.identifier, identifier)
           )
         );
       return row;

Add the import:

// at top of file
import { and, eq, gt } from "drizzle-orm";

Note: NextAuth typically checks expiry in-core, but enforcing it at the DB predicate is a clean defense-in-depth move.

packages/database/auth/auth-options.tsx (2)

37-40: Don’t enable NextAuth debug in production

Always-on debug can leak sensitive information in logs. Gate it on environment.

-    debug: true,
+    debug: process.env.NODE_ENV !== "production",

77-112: Add server-side rate limiting/throttling for OTP requests

Client-side cool-downs are insufficient against automated abuse. Add server-side rate limiting for sendVerificationRequest, keyed by identifier and IP (e.g., Redis/Upstash sliding window, or a DB-based counter with short TTL). Include backoff and consistent 429 responses.

I can draft a minimal Redis-backed limiter for this entry point if you confirm the preferred store (e.g., Upstash Redis vs. DB).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3b3f87d and ae79a17.

📒 Files selected for processing (5)
  • apps/web/app/(org)/login/form.tsx (4 hunks)
  • apps/web/app/(org)/verify-otp/form.tsx (1 hunks)
  • packages/database/auth/auth-options.tsx (1 hunks)
  • packages/database/auth/drizzle-adapter.ts (1 hunks)
  • packages/database/schema.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/app/(org)/verify-otp/form.tsx
  • apps/web/app/(org)/login/form.tsx
🧰 Additional context used
🧬 Code Graph Analysis (3)
packages/database/schema.ts (3)
packages/database/helpers.ts (3)
  • nanoId (6-9)
  • nanoIdLength (3-3)
  • VideoMetadata (21-24)
packages/web-domain/src/Folder.ts (2)
  • FolderId (7-7)
  • FolderId (8-8)
packages/database/types/metadata.ts (1)
  • VideoMetadata (8-44)
packages/database/auth/auth-options.tsx (3)
packages/database/auth/drizzle-adapter.ts (1)
  • DrizzleAdapter (8-254)
packages/database/emails/otp-email.tsx (1)
  • OTPEmail (16-64)
packages/database/emails/config.ts (1)
  • sendEmail (8-43)
packages/database/auth/drizzle-adapter.ts (3)
packages/database/schema.ts (4)
  • users (45-87)
  • accounts (89-114)
  • sessions (116-130)
  • verificationTokens (132-138)
packages/database/helpers.ts (1)
  • nanoId (6-9)
packages/utils/src/lib/stripe/stripe.ts (2)
  • STRIPE_AVAILABLE (8-8)
  • stripe (9-16)
🪛 Biome (2.1.2)
packages/database/schema.ts

[error] 450-450: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (javascript-typescript)

Comment on lines 81 to 112
async sendVerificationRequest({ identifier, token }) {
console.log("sendVerificationRequest");

if (!serverEnv().RESEND_API_KEY) {
console.log("\n");
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log("🔐 VERIFICATION CODE (Development Mode)");
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log(`📧 Email: ${identifier}`);
console.log(`🔢 Code: ${token}`);
console.log(`⏱️ Expires in: 10 minutes`);
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log("\n");
} else {
console.log({ identifier, token });
const { OTPEmail } = await import("../emails/otp-email");
const email = OTPEmail({ code: token, email: identifier });
console.log({ email });
await sendEmail({
email: identifier,
subject: `Your Cap Verification Code`,
react: email,
});
}
},
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove sensitive logging in production path (identifier, token, rendered email)

The else-branch (production path) logs the identifier, token, and rendered email. This is sensitive and should not be logged.

-          async sendVerificationRequest({ identifier, token }) {
-            console.log("sendVerificationRequest");
+          async sendVerificationRequest({ identifier, token }) {
             if (!serverEnv().RESEND_API_KEY) {
               console.log("\n");
               console.log(
                 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
               );
               console.log("🔐 VERIFICATION CODE (Development Mode)");
               console.log(
                 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
               );
               console.log(`📧 Email: ${identifier}`);
               console.log(`🔢 Code: ${token}`);
               console.log(`⏱️  Expires in: 10 minutes`);
               console.log(
                 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
               );
               console.log("\n");
             } else {
-              console.log({ identifier, token });
               const { OTPEmail } = await import("../emails/otp-email");
-              const email = OTPEmail({ code: token, email: identifier });
-              console.log({ email });
               await sendEmail({
                 email: identifier,
                 subject: `Your Cap Verification Code`,
-                react: email,
+                react: OTPEmail({ code: token, email: identifier }),
               });
             }
           },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async sendVerificationRequest({ identifier, token }) {
console.log("sendVerificationRequest");
if (!serverEnv().RESEND_API_KEY) {
console.log("\n");
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log("🔐 VERIFICATION CODE (Development Mode)");
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log(`📧 Email: ${identifier}`);
console.log(`🔢 Code: ${token}`);
console.log(`⏱️ Expires in: 10 minutes`);
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log("\n");
} else {
console.log({ identifier, token });
const { OTPEmail } = await import("../emails/otp-email");
const email = OTPEmail({ code: token, email: identifier });
console.log({ email });
await sendEmail({
email: identifier,
subject: `Your Cap Verification Code`,
react: email,
});
}
},
}),
async sendVerificationRequest({ identifier, token }) {
if (!serverEnv().RESEND_API_KEY) {
console.log("\n");
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log("🔐 VERIFICATION CODE (Development Mode)");
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log(`📧 Email: ${identifier}`);
console.log(`🔢 Code: ${token}`);
console.log(`⏱️ Expires in: 10 minutes`);
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
);
console.log("\n");
} else {
const { OTPEmail } = await import("../emails/otp-email");
await sendEmail({
email: identifier,
subject: `Your Cap Verification Code`,
react: OTPEmail({ code: token, email: identifier }),
});
}
},
🤖 Prompt for AI Agents
In packages/database/auth/auth-options.tsx around lines 81 to 112, the
production branch currently logs sensitive data (identifier, token, rendered
email); remove those console.log calls so no sensitive values are emitted in
production. Specifically, delete the console.log({ identifier, token }) and
console.log({ email }) lines (or wrap them behind a strict non-production/debug
flag) and keep only the sendEmail call and minimal non-sensitive logging if
needed (e.g., "verification email sent" with no identifiers).

Comment on lines 27 to 41
if (STRIPE_AVAILABLE()) {
const customer = await stripe().customers.create({
email: userData.email,
metadata: {
userId: nanoId(),
},
});

await db
.update(users)
.set({
stripeCustomerId: customer.id,
})
.where(eq(users.id, row.id));
}
await db
.update(users)
.set({
stripeCustomerId: customer.id,
})
.where(eq(users.id, row.id));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Stripe metadata uses a new nanoId instead of the created user’s id

This writes a random userId into Stripe customer metadata, losing the linkage to the actual user. Use the newly created row.id.

-      const customer = await stripe().customers.create({
-        email: userData.email,
-        metadata: {
-          userId: nanoId(),
-        },
-      });
+      const customer = await stripe().customers.create({
+        email: userData.email,
+        metadata: {
+          userId: row.id,
+        },
+      });

Optionally, add a try/catch to avoid failing user creation when Stripe is temporarily unavailable. I can propose a guarded version if desired.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (STRIPE_AVAILABLE()) {
const customer = await stripe().customers.create({
email: userData.email,
metadata: {
userId: nanoId(),
},
});
await db
.update(users)
.set({
stripeCustomerId: customer.id,
})
.where(eq(users.id, row.id));
}
await db
.update(users)
.set({
stripeCustomerId: customer.id,
})
.where(eq(users.id, row.id));
}
if (STRIPE_AVAILABLE()) {
const customer = await stripe().customers.create({
email: userData.email,
metadata: {
userId: row.id,
},
});
await db
.update(users)
.set({
stripeCustomerId: customer.id,
})
.where(eq(users.id, row.id));
}
🤖 Prompt for AI Agents
In packages/database/auth/drizzle-adapter.ts around lines 27 to 41, the code
writes a new nanoId into Stripe customer.metadata.userId instead of using the
actual created user's id (row.id); replace nanoId() with row.id so
metadata.userId is the real user id, and wrap the Stripe customer creation and
subsequent db.update in a try/catch so Stripe failures are logged/handled
without aborting user creation (i.e., attempt Stripe only if STRIPE_AVAILABLE(),
catch and log the error, and proceed).

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.

3 participants