feat: complete email verification and SEO setup - #75
Conversation
📝 WalkthroughWalkthroughImplements email verification for user authentication across the application. Adds custom signup API, email verification flow pages, token management, email sending utilities, SEO configuration with sitemap and Open Graph image, PWA manifest, dark theme refinements, and structured data components for search visibility. Changes
Sequence DiagramsequenceDiagram
actor User
participant Client as Browser
participant SignupAPI as POST /api/auth/custom-signup
participant Database as Prisma / Database
participant EmailService as Email Service
participant VerifyAPI as POST /api/auth/verify-email
participant AuthService as Auth Service
User->>Client: Fill signup form
Client->>SignupAPI: POST email, password, name
SignupAPI->>Database: Check if user exists
alt User Already Verified
SignupAPI-->>Client: 400 Email exists
else User Unverified/New
SignupAPI->>SignupAPI: Hash password, generate token & expiry
SignupAPI->>Database: Create/update user with token
SignupAPI->>EmailService: Send verification email
EmailService-->>SignupAPI: Email sent (fire & forget)
SignupAPI-->>Client: 200 {success, email}
end
Client->>Client: Redirect to /check-email
User->>User: Check inbox, click link
Client->>VerifyAPI: POST token from query param
VerifyAPI->>Database: Lookup user by emailVerifyToken
VerifyAPI->>VerifyAPI: Validate token expiry
alt Token Valid & Not Expired
VerifyAPI->>Database: Mark emailVerified true, clear token
VerifyAPI-->>Client: 200 {success}
Client->>Client: Redirect to /login?verified=true
User->>Client: User logs in
Client->>AuthService: Sign in with verified email
AuthService-->>Client: Grant session
else Token Expired/Invalid
VerifyAPI-->>Client: 400 {error: TOKEN_EXPIRED}
Client->>Client: Show error, link to resend
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81ce92319b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| name: "Schedule Trigger Poller", | ||
| }, | ||
| { cron: "* * * * *" }, | ||
| { cron: "0 9 * * *" }, |
There was a problem hiding this comment.
Restore per-minute cadence for schedule poller
The poller now runs at 0 9 * * *, but the execution logic only fires triggers when secondsSincePrev <= 60. With a once-daily cron, almost all schedules (hourly, every 5 minutes, etc.) will never satisfy that window, so scheduled workflows effectively stop running except around 09:00 UTC. This is a functional regression for production scheduling.
Useful? React with 👍 / 👎.
| emailVerifyToken String? @unique | ||
| emailVerifyExpiry DateTime? | ||
| emailVerifyAttempts Int @default(0) |
There was a problem hiding this comment.
Add Prisma migration for email verification fields
New User columns (emailVerifyToken, emailVerifyExpiry, emailVerifyAttempts) are introduced and used by the new auth routes, but this commit does not add a corresponding file under prisma/migrations. Environments that apply schema changes via migrations will not have these columns, causing runtime DB errors on signup/resend/verify flows after deployment.
Useful? React with 👍 / 👎.
| data: { | ||
| emailVerifyToken: token, | ||
| emailVerifyExpiry: getTokenExpiry(), | ||
| emailVerifyAttempts: 0, |
There was a problem hiding this comment.
Preserve resend-attempt limits in custom signup flow
When an unverified account signs up again, this path resets emailVerifyAttempts to 0 before sending another verification email. That bypasses the max 3 resends protection in /api/auth/resend-verification, so repeated calls to custom signup can trigger unlimited verification emails to the same address.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (5)
src/lib/email-verification.ts (1)
28-36: Consider extracting the SMTP transporter to avoid recreating it on every send.Both
sendVerificationEmailandsendResendVerificationEmailcreate a newnodemailer.createTransport()instance on each call. This adds overhead and prevents connection pooling.♻️ Suggested refactor
// Create transporter once at module level const getTransporter = () => { return nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT || "587"), secure: process.env.SMTP_SECURE === "true", auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }) } // Lazy singleton pattern if needed let transporter: ReturnType<typeof nodemailer.createTransport> | null = null function getMailer() { if (!transporter) { transporter = getTransporter() } return transporter }Then use
getMailer().sendMail(...)in both functions.Also applies to: 119-127
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/email-verification.ts` around lines 28 - 36, Extract the nodemailer transporter creation out of the per-call code and implement a module-level lazy singleton (e.g., add functions getTransporter()/getMailer() that construct nodemailer.createTransport(...) using the same host/port/secure/auth config shown and cache the instance in a local variable), then replace the transporter creation inside sendVerificationEmail and sendResendVerificationEmail with calls to getMailer().sendMail(...); this avoids recreating the transporter on every send and enables connection pooling.src/app/api/auth/custom-signup/route.ts (3)
74-79: Misleading comment: email sending is blocking.The comment says "non-blocking" but
await sendVerificationEmail(...)is used. The operation blocks execution but errors are swallowed, which is likely the intended behavior. Consider clarifying the comment.📝 Suggested comment fix
- // 5. Send verification email (non-blocking) + // 5. Send verification email (errors logged but not thrown) try { await sendVerificationEmail(email, user.name || email, verifyToken) } catch (emailError) { console.error("Failed to send verification email:", emailError) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/api/auth/custom-signup/route.ts` around lines 74 - 79, The comment is misleading: the current code uses await on sendVerificationEmail(email, user.name || email, verifyToken) so the call is blocking even though errors are caught; update the comment above this block in route.ts to accurately describe the behavior (e.g., "Send verification email (awaited; errors are caught and logged)" or "Send verification email (fire-and-forget — remove await and catch errors on the promise)" depending on desired behavior), and ensure the text references the sendVerificationEmail call and that errors are logged via the existing catch block.
21-38: Consider whether to update password for unverified accounts on re-signup.When an unverified user attempts to sign up again, the token is regenerated but the password is not updated. If users forgot the password they used initially, they cannot change it until after verification.
This may be intentional (prevents account takeover via re-signup), but consider documenting this behavior or providing a "forgot password" flow for unverified accounts.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/api/auth/custom-signup/route.ts` around lines 21 - 38, The current re-signup branch regenerates a verification token (generateVerifyToken) and updates the user record via prisma.user.update (emailVerifyToken, emailVerifyExpiry, emailVerifyAttempts) but does not update the password, which prevents users who forgot their original password from changing it before verification; decide and implement the chosen behavior: either (A) update the password hash in prisma.user.update when a new password is supplied (validate and hash the new password before saving) and keep the rest of the resend logic including sendVerificationEmail, or (B) explicitly preserve the existing behavior and add documentation/comments and/or return a specific response prompting the user to use a "forgot password" flow for unverified accounts; reference generateVerifyToken, prisma.user.update, and sendVerificationEmail when making the change so reviewers can locate the code to update.
9-14: Add server-side input validation for email format and password strength.The endpoint only checks for presence of
passwordbut doesn't validate email format or enforce password strength requirements. Relying solely on client-side validation can be bypassed.🛡️ Proposed validation additions
+import { z } from "zod" + +const signupSchema = z.object({ + email: z.email(), + password: z.string().min(8), + name: z.string().optional(), +}) + export async function POST(req: NextRequest) { try { const body = await req.json() - const { email, password, name } = body - - if (!email || !password) { - return NextResponse.json({ error: "Missing email or password" }, { status: 400 }) - } + const parsed = signupSchema.safeParse(body) + if (!parsed.success) { + return NextResponse.json({ error: "Invalid email or password" }, { status: 400 }) + } + const { email, password, name } = parsed.data🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/api/auth/custom-signup/route.ts` around lines 9 - 14, The signup handler currently only checks presence of email/password; add server-side validation in the same route (route.ts): implement an isValidEmail(email) check (simple RFC-like regex) and an isStrongPassword(password) check (e.g., min length 8, at least one uppercase, one lowercase, one digit and one special char), and return NextResponse.json({...}, { status: 400 }) with a clear error message when either check fails; you can add small helper functions inside the file (e.g., isValidEmail and isStrongPassword) and use them after extracting const { email, password, name } = body before proceeding with account creation.src/app/globals.css (1)
332-335: Consider respectingprefers-reduced-motionfor accessibility.Global smooth scrolling can cause discomfort for users with vestibular disorders. Consider conditionally disabling it when reduced motion is preferred.
♿ Proposed accessibility improvement
/* Smooth scroll globally */ html { scroll-behavior: smooth; } + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/globals.css` around lines 332 - 335, The global CSS rule setting smooth scrolling on the html selector (html { scroll-behavior: smooth; }) should respect users' prefers-reduced-motion preference; add a media query for `@media` (prefers-reduced-motion: reduce) that targets the same html selector and sets scroll-behavior to auto (or initial) so smooth scrolling is disabled for users who request reduced motion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@next-sitemap.config.js`:
- Around line 18-25: The additionalPaths array in next-sitemap.config.js is
including hardcoded routes via config.transform (additionalPaths and
config.transform) that don't exist — specifically "/pricing", "/blog", and
"/integrations" — causing 404s; remove those entries from the additionalPaths
list (keep only valid transforms like "/" and "/login", "/signup" or add real
page files if you intend to publish them) so update additionalPaths to stop
calling await config.transform for the missing routes.
In `@prisma/schema.prisma`:
- Around line 22-25: The schema adds fields emailVerifyToken, emailVerifyExpiry,
and emailVerifyAttempts but no DB migration exists; run a Prisma migration to
add these columns by executing the Prisma migrate dev flow (create a migration
named add_email_verification_fields), verify the generated SQL includes the
three fields for the model containing emailVerified, apply the migration to the
database, and commit the new migration files so runtime Prisma queries against
emailVerifyToken/emailVerifyExpiry/emailVerifyAttempts succeed.
In `@public/manifest.json`:
- Around line 9-12: The manifest references two PWA icon files that are missing
and should be added: create the image assets named favicon-192.png and
favicon-512.png in the public/ directory (ensure they match the declared sizes
192x192 and 512x512 and are valid PNGs) and update the manifest icon entries
(the "icons" array in manifest.json) to include at least one entry with
"purpose": "maskable" (e.g., add "purpose": "maskable" to the 192 or 512 icon
object) so Android adaptive icons are supported and no 404s occur.
In `@src/app/`(auth)/verify-email/page.tsx:
- Around line 15-46: The effect in useEffect that posts to
/api/auth/verify-email must be made single-shot and cancellable: add a ref
(e.g., hasRunRef) to guard so the fetch block runs only once even in React
Strict Mode, create an AbortController for the fetch and pass controller.signal
to fetch to allow aborting, store the redirect timeout id (from setTimeout) and
clearTimeout in the effect cleanup, and call controller.abort() in cleanup;
update references to token, router, setStatus, and setMessage accordingly so the
effect exits early if token is missing or hasRunRef.current is true.
In `@src/app/api/auth/custom-signup/route.ts`:
- Around line 52-72: The user and account creations must be atomic: instead of
calling prisma.user.create and then prisma.account.create separately, wrap both
creates in a single Prisma transaction (prisma.$transaction) so that creating
the user (id via createId(), emailVerified fields, emailVerifyToken/expiry) and
creating the associated account (id via createId(), userId referencing the
created user's id, providerId "credential", and password hashedPassword) succeed
or roll back together; update the code around prisma.user.create and
prisma.account.create to build both create operations and execute them inside
prisma.$transaction, returning/using the created user info from the transaction
result.
In `@src/app/api/auth/resend-verification/route.ts`:
- Around line 24-35: The code updates the user's
emailVerifyToken/emailVerifyExpiry and increments emailVerifyAttempts before
sendResendVerificationEmail completes, which can consume an attempt on transient
email failures; fix by deferring the prisma.user.update until after
sendResendVerificationEmail succeeds (call generateVerifyToken(), call
sendResendVerificationEmail(email, user.name || email, token) first, then run
prisma.user.update to set emailVerifyToken, emailVerifyExpiry (getTokenExpiry())
and increment emailVerifyAttempts), or alternatively wrap the send+update in a
transaction/try-catch that rolls back or only commits the token/attempt update
on successful send; refer to generateVerifyToken, sendResendVerificationEmail,
getTokenExpiry, and prisma.user.update/emailVerifyAttempts for where to change
the flow.
- Around line 16-21: The current lifetime counter emailVerifyAttempts in the
resend-verification handler causes a permanent lockout; change the logic to
implement a time-windowed cooldown instead: add/consume a timestamp field (e.g.,
lastEmailResendAt) and track attempts within a sliding window (e.g., count
attempts where lastEmailResendAt >= now - WINDOW); if attempts in that window
exceed MAX_PER_WINDOW return 429, otherwise increment the attempt count for the
current window and set lastEmailResendAt to now, and keep the existing
reset-on-success behavior for verification completion; update the
resend-verification handler where emailVerifyAttempts is used to reference these
new fields and window logic so users are only temporarily rate-limited rather
than permanently locked out.
- Around line 7-9: The handler currently destructures email from await
req.json() and calls prisma.user.findUnique which lets {} / null / non-string
email generate a 500; first await and store the raw payload (e.g., const payload
= await req.json()), validate that payload is an object and payload.email is a
non-empty string, and if not return a 400 response immediately; only after that
validation call prisma.user.findUnique({ where: { email: payload.email } }) so
invalid requests never hit Prisma (update the code around the existing
destructuring and prisma.user.findUnique usage).
In `@src/app/layout.tsx`:
- Around line 72-79: The OpenGraph image URL in the metadata images array uses a
relative path ("./logos/logo.png") that won't resolve in Next.js; update the
images entry (the url field inside the images array in src/app/layout.tsx /
metadata) to a root-relative path (e.g., "/logos/logo.png") or a full absolute
URL so Next.js can correctly resolve and serve the OG image.
In `@src/components/structured-data.tsx`:
- Around line 42-46: Create a shared React component named JsonLd that renders a
<script type="application/ld+json"> element from a provided data prop, moves the
JSON.stringify(data) logic there, and add a narrowly-scoped Biome rule
suppression comment (eslint-style) directly above the dangerous innerHTML usage
with a short justification like "static, controlled JSON-LD payload"; then
replace the two inline usages in the StructuredData component with <JsonLd
data={...} /> calls (identify/modify the existing render branches that currently
call dangerouslySetInnerHTML) so the lint rule is only suppressed in one
centralized place (component name: JsonLd).
In `@src/features/auth/components/login-form.tsx`:
- Around line 31-34: The file is missing imports for useSearchParams and the
React binding/Suspense; import useSearchParams from 'next/navigation' and import
React (or directly import { Suspense } from 'react'), then update the component
to use the imported Suspense symbol in place of React.Suspense (e.g., wrap the
lazy children with <Suspense> inside the LoginContent component) and ensure
useSearchParams is referenced from the imported hook.
- Around line 84-94: Replace the broad 403/status and message substring check in
the login form error handling with a direct check for the stable error code: use
ctx.error.code === "EMAIL_NOT_VERIFIED" inside the error handling block in the
login-form component (where ctx.error and toast.error are used) so only the
specific unverified-email case shows the resend-verification CTA; keep the
existing toast UI (the Link to "/resend-verification") unchanged and fall back
to toast.error(ctx.error.message) for all other errors.
In `@src/features/auth/components/register-form.tsx`:
- Around line 92-93: The signup flow currently appends the user email to the URL
in register-form.tsx via
router.push(`/check-email?email=${encodeURIComponent(values.email)}`), leaking a
PII; change the router.push call to navigate to '/check-email' without any query
string and remove any dependency on the email query in
src/app/(auth)/check-email/page.tsx (update code that reads router.query.email
to tolerate absence or use another non-URL mechanism if display of the email is
still required). Ensure router.push is updated and the check-email page no
longer expects or relies on the email query parameter.
In `@src/inngest/functions/schedule-poller.ts`:
- Line 10: The cron change to "0 9 * * *" in schedule-poller.ts breaks the
existing minute-level execution check (secondsSincePrev <= 60) and prevents most
cronExpression triggers from running; either revert the scheduled poll to a
frequent interval (e.g., every minute) so secondsSincePrev logic remains valid,
or update the poller logic to compute and process missed occurrences between the
last run and now for each user-configured cronExpression (use a cron parser to
enumerate occurrences between prevRun and now) instead of relying on the
60-second window—adjust uses of secondsSincePrev, cronExpression, and the poll
handler in schedule-poller.ts accordingly.
In `@src/lib/email-verification.ts`:
- Around line 70-72: The userName is interpolated into HTML email templates
without escaping, enabling HTML/script injection; add an escapeHtml function (as
proposed) in src/lib/email-verification.ts and replace direct uses of userName
in all templates (the verification and any other email templates in this module)
with escapeHtml(userName) so the rendered HTML encodes &, <, >, ", and '
characters; ensure you call the same escapeHtml for every occurrence of userName
in this file (verification templates and any other message bodies) and export or
keep the helper local as needed.
---
Nitpick comments:
In `@src/app/api/auth/custom-signup/route.ts`:
- Around line 74-79: The comment is misleading: the current code uses await on
sendVerificationEmail(email, user.name || email, verifyToken) so the call is
blocking even though errors are caught; update the comment above this block in
route.ts to accurately describe the behavior (e.g., "Send verification email
(awaited; errors are caught and logged)" or "Send verification email
(fire-and-forget — remove await and catch errors on the promise)" depending on
desired behavior), and ensure the text references the sendVerificationEmail call
and that errors are logged via the existing catch block.
- Around line 21-38: The current re-signup branch regenerates a verification
token (generateVerifyToken) and updates the user record via prisma.user.update
(emailVerifyToken, emailVerifyExpiry, emailVerifyAttempts) but does not update
the password, which prevents users who forgot their original password from
changing it before verification; decide and implement the chosen behavior:
either (A) update the password hash in prisma.user.update when a new password is
supplied (validate and hash the new password before saving) and keep the rest of
the resend logic including sendVerificationEmail, or (B) explicitly preserve the
existing behavior and add documentation/comments and/or return a specific
response prompting the user to use a "forgot password" flow for unverified
accounts; reference generateVerifyToken, prisma.user.update, and
sendVerificationEmail when making the change so reviewers can locate the code to
update.
- Around line 9-14: The signup handler currently only checks presence of
email/password; add server-side validation in the same route (route.ts):
implement an isValidEmail(email) check (simple RFC-like regex) and an
isStrongPassword(password) check (e.g., min length 8, at least one uppercase,
one lowercase, one digit and one special char), and return
NextResponse.json({...}, { status: 400 }) with a clear error message when either
check fails; you can add small helper functions inside the file (e.g.,
isValidEmail and isStrongPassword) and use them after extracting const { email,
password, name } = body before proceeding with account creation.
In `@src/app/globals.css`:
- Around line 332-335: The global CSS rule setting smooth scrolling on the html
selector (html { scroll-behavior: smooth; }) should respect users'
prefers-reduced-motion preference; add a media query for `@media`
(prefers-reduced-motion: reduce) that targets the same html selector and sets
scroll-behavior to auto (or initial) so smooth scrolling is disabled for users
who request reduced motion.
In `@src/lib/email-verification.ts`:
- Around line 28-36: Extract the nodemailer transporter creation out of the
per-call code and implement a module-level lazy singleton (e.g., add functions
getTransporter()/getMailer() that construct nodemailer.createTransport(...)
using the same host/port/secure/auth config shown and cache the instance in a
local variable), then replace the transporter creation inside
sendVerificationEmail and sendResendVerificationEmail with calls to
getMailer().sendMail(...); this avoids recreating the transporter on every send
and enables connection pooling.
🪄 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: 87e63949-9e0a-49cc-bd4c-b906622ca523
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (20)
next-sitemap.config.jspackage.jsonprisma/schema.prismapublic/manifest.jsonsrc/app/(auth)/check-email/page.tsxsrc/app/(auth)/resend-verification/page.tsxsrc/app/(auth)/verify-email/page.tsxsrc/app/api/auth/custom-signup/route.tssrc/app/api/auth/resend-verification/route.tssrc/app/api/auth/verify-email/route.tssrc/app/globals.csssrc/app/layout.tsxsrc/app/opengraph-image.tsxsrc/components/entity-components.tsxsrc/components/structured-data.tsxsrc/features/auth/components/login-form.tsxsrc/features/auth/components/register-form.tsxsrc/inngest/functions/schedule-poller.tssrc/lib/auth.tssrc/lib/email-verification.ts
| additionalPaths: async (config) => [ | ||
| await config.transform(config, "/"), | ||
| await config.transform(config, "/pricing"), | ||
| await config.transform(config, "/blog"), | ||
| await config.transform(config, "/integrations"), | ||
| await config.transform(config, "/login"), | ||
| await config.transform(config, "/signup"), | ||
| ], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
from pathlib import Path
targets = ["/", "/pricing", "/blog", "/integrations", "/login", "/signup"]
routes = set()
for path in Path("src/app").rglob("page.*"):
if path.suffix not in {".ts", ".tsx", ".js", ".jsx"}:
continue
parts = []
for part in path.parent.relative_to("src/app").parts:
if part.startswith("(") and part.endswith(")"):
continue
if part.startswith("@"):
continue
parts.append(part)
route = "/" + "/".join(parts)
routes.add(route if route != "/" else "/")
for target in targets:
print(f"{target}: {'FOUND' if target in routes else 'MISSING'}")
PYRepository: Mayank-saraswal/nodebase
Length of output: 161
Remove hardcoded routes from additionalPaths that don't exist in the codebase.
The following routes are hardcoded in additionalPaths but do not have corresponding page files:
/pricing— MISSING/blog— MISSING/integrations— MISSING
Publishing non-existent routes to the sitemap sends crawlers to 404 pages and wastes their crawl budget. Either remove these routes from additionalPaths or create the corresponding page files.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@next-sitemap.config.js` around lines 18 - 25, The additionalPaths array in
next-sitemap.config.js is including hardcoded routes via config.transform
(additionalPaths and config.transform) that don't exist — specifically
"/pricing", "/blog", and "/integrations" — causing 404s; remove those entries
from the additionalPaths list (keep only valid transforms like "/" and "/login",
"/signup" or add real page files if you intend to publish them) so update
additionalPaths to stop calling await config.transform for the missing routes.
| emailVerified Boolean @default(false) | ||
| emailVerifyToken String? @unique | ||
| emailVerifyExpiry DateTime? | ||
| emailVerifyAttempts Int @default(0) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if any migration includes the new email verification fields
echo "Searching for migrations containing emailVerifyToken..."
rg -l "emailVerifyToken" prisma/migrations/ || echo "No migration found for emailVerifyToken"Repository: Mayank-saraswal/nodebase
Length of output: 163
🏁 Script executed:
#!/bin/bash
# List all migration directories to understand the structure
echo "=== Migration files structure ==="
ls -la prisma/migrations/ 2>/dev/null || echo "No migrations directory"
# Check the latest migration file
echo -e "\n=== Latest migration content ==="
latest_migration=$(ls -1 prisma/migrations/ | sort | tail -1)
if [ -n "$latest_migration" ]; then
echo "Latest migration: $latest_migration"
cat "prisma/migrations/$latest_migration/migration.sql" 2>/dev/null || echo "No migration.sql found"
fi
# Search for any SQL files containing these field names
echo -e "\n=== All SQL files containing verification fields ==="
find prisma/migrations -name "*.sql" -type f -exec grep -l "emailVerify" {} \; 2>/dev/null || echo "No migration files found with emailVerify"
# Check current schema.prisma for the User model
echo -e "\n=== User model in current schema.prisma ==="
rg -A 20 "model User" prisma/schema.prisma || echo "User model not found"Repository: Mayank-saraswal/nodebase
Length of output: 1858
🏁 Script executed:
#!/bin/bash
# Check the init migration SQL
echo "=== Init migration SQL content ==="
cat "prisma/migrations/20260318162830_init/migration.sql" 2>/dev/null || echo "File not found"Repository: Mayank-saraswal/nodebase
Length of output: 50380
Create a database migration for the new email verification fields.
The emailVerifyToken, emailVerifyExpiry, and emailVerifyAttempts fields are defined in the schema but do not exist in the database—the initial migration does not include them. Without a migration, Prisma queries will fail at runtime when attempting to access these columns.
Run npx prisma migrate dev --name add_email_verification_fields to create and apply the migration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@prisma/schema.prisma` around lines 22 - 25, The schema adds fields
emailVerifyToken, emailVerifyExpiry, and emailVerifyAttempts but no DB migration
exists; run a Prisma migration to add these columns by executing the Prisma
migrate dev flow (create a migration named add_email_verification_fields),
verify the generated SQL includes the three fields for the model containing
emailVerified, apply the migration to the database, and commit the new migration
files so runtime Prisma queries against
emailVerifyToken/emailVerifyExpiry/emailVerifyAttempts succeed.
| "icons": [ | ||
| { "src": "/favicon-192.png", "sizes": "192x192", "type": "image/png" }, | ||
| { "src": "/favicon-512.png", "sizes": "512x512", "type": "image/png" } | ||
| ] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify icon files exist in public directory
echo "Checking for PWA icon files..."
fd -t f "favicon-192.png" public/
fd -t f "favicon-512.png" public/Repository: Mayank-saraswal/nodebase
Length of output: 98
🏁 Script executed:
#!/bin/bash
# List all files in public directory
echo "=== Files in public/ directory ==="
ls -la public/ 2>/dev/null || echo "public/ directory not found"
# Search for favicon files anywhere in repo
echo -e "\n=== Searching for favicon files in repo ==="
fd "favicon" . --type f 2>/dev/null | head -20
# Check public directory structure
echo -e "\n=== public/ directory structure ==="
find public -type f 2>/dev/null | sortRepository: Mayank-saraswal/nodebase
Length of output: 1633
🏁 Script executed:
cat -n public/manifest.jsonRepository: Mayank-saraswal/nodebase
Length of output: 538
Missing PWA icon files: create /favicon-192.png and /favicon-512.png in the public/ directory.
The manifest references /favicon-192.png and /favicon-512.png, but these files do not exist. Browsers and PWA clients will receive 404 errors when attempting to fetch these icons. Additionally, consider adding "purpose": "maskable" to at least one icon entry for better Android adaptive icon support.
Current manifest snippet (lines 9-12)
"icons": [
{ "src": "/favicon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon-512.png", "sizes": "512x512", "type": "image/png" }
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@public/manifest.json` around lines 9 - 12, The manifest references two PWA
icon files that are missing and should be added: create the image assets named
favicon-192.png and favicon-512.png in the public/ directory (ensure they match
the declared sizes 192x192 and 512x512 and are valid PNGs) and update the
manifest icon entries (the "icons" array in manifest.json) to include at least
one entry with "purpose": "maskable" (e.g., add "purpose": "maskable" to the 192
or 512 icon object) so Android adaptive icons are supported and no 404s occur.
| useEffect(() => { | ||
| if (!token) { | ||
| setStatus("error") | ||
| setMessage("No verification token found. Check your email for the correct link.") | ||
| return | ||
| } | ||
|
|
||
| // Call the verify API | ||
| fetch("/api/auth/verify-email", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ token }), | ||
| }) | ||
| .then(res => res.json()) | ||
| .then(data => { | ||
| if (data.success) { | ||
| setStatus("success") | ||
| setMessage("Email verified! Redirecting to login...") | ||
| setTimeout(() => router.push("/login?verified=true"), 2000) | ||
| } else if (data.error === "TOKEN_EXPIRED") { | ||
| setStatus("expired") | ||
| setMessage("This verification link has expired. Request a new one below.") | ||
| } else { | ||
| setStatus("error") | ||
| setMessage(data.error || "Verification failed. The link may be invalid.") | ||
| } | ||
| }) | ||
| .catch(() => { | ||
| setStatus("error") | ||
| setMessage("Network error. Please try again.") | ||
| }) | ||
| }, [token, router]) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/app/\(auth\)/verify-email/page.tsxRepository: Mayank-saraswal/nodebase
Length of output: 4113
🏁 Script executed:
find . -type f -name "*verify*" -path "*/api/*" | head -20Repository: Mayank-saraswal/nodebase
Length of output: 50
🏁 Script executed:
rg "verify-email" --type ts --type tsx --type js -lRepository: Mayank-saraswal/nodebase
Length of output: 95
🏁 Script executed:
rg "verify-email" -l --max-count=20Repository: Mayank-saraswal/nodebase
Length of output: 157
🏁 Script executed:
find . -path "*/api/*" -type f | grep -E "\.(ts|js)$" | head -30Repository: Mayank-saraswal/nodebase
Length of output: 866
🏁 Script executed:
cat -n src/app/api/auth/verify-email/route.tsRepository: Mayank-saraswal/nodebase
Length of output: 1892
Make the verification effect single-shot and cancellable.
This POST is non-idempotent—on first call the token is consumed and cleared, but on second call (which will occur in React 18 Strict Mode development) it returns an error, flipping a successful verification to a failure state. Additionally, the setTimeout is not cleaned up on unmount, risking a redirect after the page is gone. Guard the request with a ref to ensure it only runs once, and clean up the timeout and fetch using AbortController.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(auth)/verify-email/page.tsx around lines 15 - 46, The effect in
useEffect that posts to /api/auth/verify-email must be made single-shot and
cancellable: add a ref (e.g., hasRunRef) to guard so the fetch block runs only
once even in React Strict Mode, create an AbortController for the fetch and pass
controller.signal to fetch to allow aborting, store the redirect timeout id
(from setTimeout) and clearTimeout in the effect cleanup, and call
controller.abort() in cleanup; update references to token, router, setStatus,
and setMessage accordingly so the effect exits early if token is missing or
hasRunRef.current is true.
| const user = await prisma.user.create({ | ||
| data: { | ||
| id: createId(), | ||
| email, | ||
| name: name || email.split("@")[0], | ||
| emailVerified: false, | ||
| emailVerifyToken: verifyToken, | ||
| emailVerifyExpiry: verifyExpiry, | ||
| }, | ||
| }) | ||
|
|
||
| // Also create account for password login | ||
| await prisma.account.create({ | ||
| data: { | ||
| id: createId(), | ||
| userId: user.id, | ||
| accountId: email, | ||
| providerId: "credential", | ||
| password: hashedPassword, | ||
| } | ||
| }) |
There was a problem hiding this comment.
Wrap user and account creation in a transaction for atomicity.
If account creation fails after user creation succeeds, the database will have an orphan user record without login credentials. Use Prisma's $transaction to ensure both operations succeed or fail together.
🔧 Proposed fix using transaction
- const user = await prisma.user.create({
- data: {
- id: createId(),
- email,
- name: name || email.split("@")[0],
- emailVerified: false,
- emailVerifyToken: verifyToken,
- emailVerifyExpiry: verifyExpiry,
- },
- })
-
- // Also create account for password login
- await prisma.account.create({
- data: {
- id: createId(),
- userId: user.id,
- accountId: email,
- providerId: "credential",
- password: hashedPassword,
- }
- })
+ const user = await prisma.$transaction(async (tx) => {
+ const newUser = await tx.user.create({
+ data: {
+ id: createId(),
+ email,
+ name: name || email.split("@")[0],
+ emailVerified: false,
+ emailVerifyToken: verifyToken,
+ emailVerifyExpiry: verifyExpiry,
+ },
+ })
+
+ await tx.account.create({
+ data: {
+ id: createId(),
+ userId: newUser.id,
+ accountId: email,
+ providerId: "credential",
+ password: hashedPassword,
+ }
+ })
+
+ return newUser
+ })📝 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.
| const user = await prisma.user.create({ | |
| data: { | |
| id: createId(), | |
| email, | |
| name: name || email.split("@")[0], | |
| emailVerified: false, | |
| emailVerifyToken: verifyToken, | |
| emailVerifyExpiry: verifyExpiry, | |
| }, | |
| }) | |
| // Also create account for password login | |
| await prisma.account.create({ | |
| data: { | |
| id: createId(), | |
| userId: user.id, | |
| accountId: email, | |
| providerId: "credential", | |
| password: hashedPassword, | |
| } | |
| }) | |
| const user = await prisma.$transaction(async (tx) => { | |
| const newUser = await tx.user.create({ | |
| data: { | |
| id: createId(), | |
| email, | |
| name: name || email.split("@")[0], | |
| emailVerified: false, | |
| emailVerifyToken: verifyToken, | |
| emailVerifyExpiry: verifyExpiry, | |
| }, | |
| }) | |
| await tx.account.create({ | |
| data: { | |
| id: createId(), | |
| userId: newUser.id, | |
| accountId: email, | |
| providerId: "credential", | |
| password: hashedPassword, | |
| } | |
| }) | |
| return newUser | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/auth/custom-signup/route.ts` around lines 52 - 72, The user and
account creations must be atomic: instead of calling prisma.user.create and then
prisma.account.create separately, wrap both creates in a single Prisma
transaction (prisma.$transaction) so that creating the user (id via createId(),
emailVerified fields, emailVerifyToken/expiry) and creating the associated
account (id via createId(), userId referencing the created user's id, providerId
"credential", and password hashedPassword) succeed or roll back together; update
the code around prisma.user.create and prisma.account.create to build both
create operations and execute them inside prisma.$transaction, returning/using
the created user info from the transaction result.
| function LoginContent(){ | ||
| const router = useRouter(); | ||
| const searchParams = useSearchParams(); | ||
| const isVerified = searchParams.get("verified") === "true"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/features/auth/components/login-form.tsx | head -50Repository: Mayank-saraswal/nodebase
Length of output: 1794
🏁 Script executed:
tail -20 src/features/auth/components/login-form.tsxRepository: Mayank-saraswal/nodebase
Length of output: 355
🏁 Script executed:
sed -n '225,250p' src/features/auth/components/login-form.tsxRepository: Mayank-saraswal/nodebase
Length of output: 227
Import the required hooks and components.
useSearchParams() is used on line 33 without being imported from next/navigation, and React.Suspense on line 232 uses an unimported React binding. This code will not compile as written.
Add the following imports:
-import { useRouter } from "next/navigation"
+import { useRouter, useSearchParams } from "next/navigation"
+import { Suspense } from "react"And update the component to use the imported Suspense:
-<React.Suspense>
+<Suspense>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/auth/components/login-form.tsx` around lines 31 - 34, The file
is missing imports for useSearchParams and the React binding/Suspense; import
useSearchParams from 'next/navigation' and import React (or directly import {
Suspense } from 'react'), then update the component to use the imported Suspense
symbol in place of React.Suspense (e.g., wrap the lazy children with <Suspense>
inside the LoginContent component) and ensure useSearchParams is referenced from
the imported hook.
| // Check if the error is related to unverified email | ||
| if (ctx.error.message?.includes("EMAIL_NOT_VERIFIED") || ctx.error.status === 403) { | ||
| toast.error( | ||
| <div className="flex flex-col gap-1"> | ||
| <span>Please verify your email before logging in.</span> | ||
| <Link href="/resend-verification" className="underline font-medium">Resend verification email →</Link> | ||
| </div> | ||
| ); | ||
| } else { | ||
| toast.error(ctx.error.message); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the file and see the context around the lines in question
head -120 src/features/auth/components/login-form.tsx | tail -50Repository: Mayank-saraswal/nodebase
Length of output: 2047
🏁 Script executed:
# Let's also see the full file to understand the error handling
wc -l src/features/auth/components/login-form.tsxRepository: Mayank-saraswal/nodebase
Length of output: 115
🏁 Script executed:
# Search for where ctx.error is created/handled
rg "ctx\.error" src/features/auth/components/login-form.tsx -B 5 -A 5Repository: Mayank-saraswal/nodebase
Length of output: 1272
🏁 Script executed:
# Look for error codes or patterns in the auth system
fd . src/features/auth --type f -name "*.ts" -o -name "*.tsx" | head -20Repository: Mayank-saraswal/nodebase
Length of output: 298
🏁 Script executed:
# Find auth client/library setup
rg "authClient" src/features/auth/components/login-form.tsx -B 10 -A 2Repository: Mayank-saraswal/nodebase
Length of output: 1339
🏁 Script executed:
# Search for where authClient is defined/imported
rg "authClient\s*=" src/ -A 3Repository: Mayank-saraswal/nodebase
Length of output: 316
🏁 Script executed:
# Look for auth server/middleware that handles sign-in
find src -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs rg "EMAIL_NOT_VERIFIED" -lRepository: Mayank-saraswal/nodebase
Length of output: 111
🏁 Script executed:
# Look for all 403 related errors in auth
rg "403" src/features/auth -A 2 -B 2Repository: Mayank-saraswal/nodebase
Length of output: 582
🏁 Script executed:
# Search for auth server/routes that handle email verification
find src -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*/routes/*" | head -20Repository: Mayank-saraswal/nodebase
Length of output: 50
🏁 Script executed:
# Look for auth routes or server logic
find src -type f -name "*route*" -o -name "*auth*" | grep -v node_modules | grep -v ".next"Repository: Mayank-saraswal/nodebase
Length of output: 2442
🏁 Script executed:
# Check if there's a backend auth setup
rg "EMAIL_NOT_VERIFIED" -r src --type ts --type tsxRepository: Mayank-saraswal/nodebase
Length of output: 95
🏁 Script executed:
# Look for better-auth setup or custom auth handlers
rg "better-auth\|betterAuth" src -A 5 -B 2Repository: Mayank-saraswal/nodebase
Length of output: 50
🏁 Script executed:
# Search for server-side auth handlers
find src -type f -name "*.ts" | xargs rg "signIn|email.*password" -l | head -10Repository: Mayank-saraswal/nodebase
Length of output: 239
🏁 Script executed:
# Read the main auth route handler
cat src/app/api/auth/\[...all\]/route.tsRepository: Mayank-saraswal/nodebase
Length of output: 204
🏁 Script executed:
# Read custom signup route
cat src/app/api/auth/custom-signup/route.tsRepository: Mayank-saraswal/nodebase
Length of output: 3070
🏁 Script executed:
# Read the auth library setup
cat src/lib/auth.tsRepository: Mayank-saraswal/nodebase
Length of output: 1532
🏁 Script executed:
# Check auth-client setup
cat src/lib/auth-client.tsRepository: Mayank-saraswal/nodebase
Length of output: 330
🏁 Script executed:
# Check if there are any other 403 status codes returned in auth system
rg "status.*403\|403.*status" src/app/api/auth src/lib/authRepository: Mayank-saraswal/nodebase
Length of output: 120
🏁 Script executed:
# Search for error handling in better-auth or verify-email endpoint
cat src/app/api/auth/verify-email/route.tsRepository: Mayank-saraswal/nodebase
Length of output: 1486
🏁 Script executed:
# Look for any custom auth logic that might return 403
rg "403\|EMAIL_NOT_VERIFIED" src --type tsRepository: Mayank-saraswal/nodebase
Length of output: 50
🏁 Script executed:
# Search for how better-auth handles email verification errors
rg "emailVerif\|EMAIL_NOT" src -B 3 -A 3Repository: Mayank-saraswal/nodebase
Length of output: 50
🏁 Script executed:
# Search for any error code patterns or better-auth usage in the codebase
rg "error\.\w+" src/features/auth/components/login-form.tsxRepository: Mayank-saraswal/nodebase
Length of output: 361
🏁 Script executed:
# Look for better-auth documentation or types in node_modules or package.json
cat package.json | grep -i "better-auth\|auth"Repository: Mayank-saraswal/nodebase
Length of output: 177
🏁 Script executed:
# Search for all error handling patterns in the codebase
rg "ctx\.error\." src -B 2 -A 2 | head -40Repository: Mayank-saraswal/nodebase
Length of output: 2433
🏁 Script executed:
# Check if there's a types file or documentation about auth errors
find src -name "*type*auth*" -o -name "*auth*type*"Repository: Mayank-saraswal/nodebase
Length of output: 50
🏁 Script executed:
# Look at what properties are available on ctx.error by searching for error object usage
rg "onError.*ctx" src -A 10 | head -60Repository: Mayank-saraswal/nodebase
Length of output: 4348
🏁 Script executed:
# Search for better-auth package info or any imports that might show error handling
rg "better-auth" src/lib/auth.ts -A 30Repository: Mayank-saraswal/nodebase
Length of output: 1043
🏁 Script executed:
# Look for any middleware or custom error handling
rg "requireEmailVerification\|emailVerified" src -B 3 -A 3Repository: Mayank-saraswal/nodebase
Length of output: 50
🏁 Script executed:
# Check if there's any documentation or comments about auth errors
rg "EMAIL_NOT_VERIFIED\|403\|Forbidden" src --type ts -B 2 -A 2Repository: Mayank-saraswal/nodebase
Length of output: 50
🏁 Script executed:
# Search web to understand better-auth error codesRepository: Mayank-saraswal/nodebase
Length of output: 50
🌐 Web query:
better-auth v1.2 email verification error code message
💡 Result:
In Better Auth v1.2, when a user tries to sign in but email verification is required (emailAndPassword.requireEmailVerification: true), the sign-in fails with HTTP 403, and the error corresponds to:
- Error code:
EMAIL_NOT_VERIFIED - Default message:
"Email not verified"[2]
In the email verification guide, Better Auth shows handling this case by checking for status === 403 and displaying your own message (or using ctx.error.message). [1]
Sources:
[1] Better Auth docs – Email verification (sign-in without verification returns 403) (better-auth.com)
[2] Better Auth docs – i18n plugin (error code → default message mapping, includes EMAIL_NOT_VERIFIED) (better-auth.com)
Citations:
🏁 Script executed:
# Check what error properties are available from better-auth
rg "error\." src/features/auth/components -B 2 -A 2 | grep -E "error\.\w+" -o | sort -uRepository: Mayank-saraswal/nodebase
Length of output: 94
🌐 Web query:
better-auth error object properties ctx.error.code errorCode structure
💡 Result:
In Better Auth, the “error code” you’ll see depends on where you’re handling the error:
1) Client calls / fetchOptions.onError(ctx) / hooks (useSession)
In the client onError(ctx) callback, ctx.error is a flat object and the code is on ctx.error.code (not errorCode):
onError(ctx) {
// ctx.error shape:
// { message, code, status, statusText }
console.log(ctx.error.code)
}Example logged shape: { message: "...", code: "INVALID_EMAIL_OR_PASSWORD", status: 401, statusText: "UNAUTHORIZED" }. [1] [2]
Better Auth’s client docs describe the error object as having message, status, and statusText, and their examples show error.code for code-based handling/translation. [1]
2) Redirect-to-error-page errors (/api/auth/error)
These are different: the “error code” is typically a snake_case string like invalid_callback_request, state_mismatch, etc. (these are the ones listed in the Errors reference). [3]
3) Plugin / $ERROR_CODES structure (newer versions)
If you’re defining plugin error codes, $ERROR_CODES is now expected to be RawError objects like:
{ MY_ERROR: { code: "MY_ERROR", message: "My error message" } }(using defineErrorCodes()), rather than plain strings. [4]
So: use ctx.error.code for client request errors; don’t expect an errorCode property there. Redirect errors and plugin error-code registries use different shapes/naming. [1] [2] [3] [4]
Sources: [1] [2] [3] [4]
Use ctx.error.code to check for unverified email errors.
ctx.error.status === 403 is too broad. Instead of checking the HTTP status, check ctx.error.code === "EMAIL_NOT_VERIFIED", which is the stable error code better-auth provides for this case when requireEmailVerification is enabled. This ensures only email verification errors trigger the resend-verification CTA and prevents false positives from other 403 responses.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/auth/components/login-form.tsx` around lines 84 - 94, Replace
the broad 403/status and message substring check in the login form error
handling with a direct check for the stable error code: use ctx.error.code ===
"EMAIL_NOT_VERIFIED" inside the error handling block in the login-form component
(where ctx.error and toast.error are used) so only the specific unverified-email
case shows the resend-verification CTA; keep the existing toast UI (the Link to
"/resend-verification") unchanged and fall back to
toast.error(ctx.error.message) for all other errors.
| if (res.ok && data.success) { | ||
| router.push(`/check-email?email=${encodeURIComponent(values.email)}`); |
There was a problem hiding this comment.
Avoid putting the signup email in the URL.
/check-email?email=... leaks a user identifier into browser history, analytics, logs, and referrers. src/app/(auth)/check-email/page.tsx:9-25 only uses it for display, so this flow can just navigate to the page without the query string.
🔐 Minimal fix
- router.push(`/check-email?email=${encodeURIComponent(values.email)}`);
+ router.push("/check-email");📝 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 (res.ok && data.success) { | |
| router.push(`/check-email?email=${encodeURIComponent(values.email)}`); | |
| if (res.ok && data.success) { | |
| router.push("/check-email"); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/auth/components/register-form.tsx` around lines 92 - 93, The
signup flow currently appends the user email to the URL in register-form.tsx via
router.push(`/check-email?email=${encodeURIComponent(values.email)}`), leaking a
PII; change the router.push call to navigate to '/check-email' without any query
string and remove any dependency on the email query in
src/app/(auth)/check-email/page.tsx (update code that reads router.query.email
to tolerate absence or use another non-URL mechanism if display of the email is
still required). Ensure router.push is updated and the check-email page no
longer expects or relies on the email query parameter.
| name: "Schedule Trigger Poller", | ||
| }, | ||
| { cron: "* * * * *" }, | ||
| { cron: "0 9 * * *" }, |
There was a problem hiding this comment.
Daily poll cadence breaks cron trigger execution semantics.
At Line 10, switching to 0 9 * * * conflicts with the minute-level firing check (secondsSincePrev <= 60 at Line 37). This causes most valid cronExpression values to never execute unless they happen to align with that single daily run window. Given cronExpression is user-configurable (prisma/schema.prisma:287-297, src/server/routers/schedule-trigger.router.ts:14-34), this is a functional regression.
💡 Suggested fix
- { cron: "0 9 * * *" },
+ { cron: "* * * * *" },If daily polling is intentional for cost reasons, then this function needs a backlog strategy (process missed occurrences since last poll) instead of a 60-second window.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/inngest/functions/schedule-poller.ts` at line 10, The cron change to "0 9
* * *" in schedule-poller.ts breaks the existing minute-level execution check
(secondsSincePrev <= 60) and prevents most cronExpression triggers from running;
either revert the scheduled poll to a frequent interval (e.g., every minute) so
secondsSincePrev logic remains valid, or update the poller logic to compute and
process missed occurrences between the last run and now for each user-configured
cronExpression (use a cron parser to enumerate occurrences between prevRun and
now) instead of relying on the 60-second window—adjust uses of secondsSincePrev,
cronExpression, and the poll handler in schedule-poller.ts accordingly.
| Hi ${userName}, click the button below to verify your email and | ||
| activate your Nodebase account. This link expires in 24 hours. | ||
| </p> |
There was a problem hiding this comment.
Escape userName to prevent HTML injection in email templates.
The userName variable is interpolated directly into HTML without escaping. If a user registers with a name containing HTML/script tags, it could render unintended content in the email.
🛡️ Proposed fix
Add an escape function and use it:
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}Then use escapeHtml(userName) in the template:
- <p style="margin:0 0 24px;color:`#999`;font-size:15px;line-height:1.6;">
- Hi ${userName}, click the button below to verify your email and
+ <p style="margin:0 0 24px;color:`#999`;font-size:15px;line-height:1.6;">
+ Hi ${escapeHtml(userName)}, click the button below to verify your email andApply the same fix to both email templates (lines 70, 102, 159, 191).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/email-verification.ts` around lines 70 - 72, The userName is
interpolated into HTML email templates without escaping, enabling HTML/script
injection; add an escapeHtml function (as proposed) in
src/lib/email-verification.ts and replace direct uses of userName in all
templates (the verification and any other email templates in this module) with
escapeHtml(userName) so the rendered HTML encodes &, <, >, ", and ' characters;
ensure you call the same escapeHtml for every occurrence of userName in this
file (verification templates and any other message bodies) and export or keep
the helper local as needed.
Summary by CodeRabbit
Release Notes
New Features
Improvements