-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: Implement OTP code, replacing magic auth #832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughIntroduces 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
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 unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 propsProps
codeshould 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 previewThe 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 performanceThe
validateOTPfunction queries by bothidentifierandcodetogether. 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
📒 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
nextparameter 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 configurationThe "10 minutes" expiration mentioned here should be synchronized with the actual
OTP_EXPIRY_MINUTESconstant 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 generationWhile
crypto.randomIntis 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 functionThe 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
ltfrom 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 definitionThe
otp_codestable is well-designed with appropriate:
- Indexes on
identifier,code, andexpiresfor query performance- Proper data types and constraints
- Automatic timestamp management
| await signIn("otp", { | ||
| email, | ||
| redirect: true, | ||
| callbackUrl: next || "/dashboard", | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| type="text" | ||
| inputMode="numeric" | ||
| pattern="[0-9]*" | ||
| maxLength={1} |
There was a problem hiding this comment.
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.
| 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.
| onPaste={(e) => { | ||
| e.preventDefault(); | ||
| const pastedData = e.clipboardData.getData("text").replace(/\D/g, ""); | ||
| handleChange(0, pastedData); | ||
| }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
packages/database/auth/otp.ts
Outdated
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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.
packages/database/auth/otp.ts
Outdated
| if (!code.match(/^\d{6}$/)) { | ||
| return { valid: false, error: "Invalid code format" }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this comment.
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:"
dateLength 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.
There was a problem hiding this 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 callbackThe 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 upstreamThe 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 userThrowing “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 checksTo 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 productionAlways-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 requestsClient-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.
📒 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)
| 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, | ||
| }); | ||
| } | ||
| }, | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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).
| 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)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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).
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
UX Improvements
Chores