Skip to content

fix(auth): address reviewer feedback and harden auth flows#143

Merged
ViktorSvertoka merged 3 commits into
developfrom
chore/auth-improvements
Jan 15, 2026
Merged

fix(auth): address reviewer feedback and harden auth flows#143
ViktorSvertoka merged 3 commits into
developfrom
chore/auth-improvements

Conversation

@kryvosheyin
Copy link
Copy Markdown
Collaborator

@kryvosheyin kryvosheyin commented Jan 14, 2026

This pull request focuses on improving the robustness and security of authentication-related pages and API routes in the frontend application. The main changes include enhanced error handling for network issues, prevention of open redirect vulnerabilities, UI improvements, and more reliable base URL resolution for email links. Additionally, there are code cleanups and minor UI adjustments for better user experience.

Security and Redirect Handling:

Error Handling Improvements:

API Improvements:

  • Switched password reset and email verification API routes to use a reliable resolveBaseUrl helper for constructing email links, ensuring correct URLs regardless of deployment environment. (frontend/app/api/auth/password-reset/route.ts, frontend/app/api/auth/resend-verification/route.ts) [1] [2] [3]
  • Simplified password reset confirmation logic by removing unnecessary database transaction wrappers. (frontend/app/api/auth/password-reset/confirm/route.ts)

UI and Code Cleanups:

Minor Enhancements:

These changes collectively improve the security, reliability, and user experience of the authentication flows.

Database Changes (if applicable)

  • Schema migration required
  • Seed data updated
  • Breaking changes to existing queries
  • Transaction-safe migration
  • Migration tested locally on Neon

How Has This Been Tested?

  • Tested locally
  • Verified in development environment
  • Checked responsive layout (if UI-related)
  • Tested accessibility (keyboard / screen reader)

Screenshots (if applicable)


Checklist

Before submitting

  • Code has been self-reviewed
  • No TypeScript or console errors
  • Code follows project conventions
  • Scope is limited to this feature/fix
  • No unrelated refactors included
  • English used in code, commits, and docs
  • New dependencies discussed with team
  • Database migration tested locally (if applicable)
  • GitHub Projects card moved to In Review

Reviewers

Summary by CodeRabbit

  • New Features

    • Safer redirect handling after auth to prevent navigation to unsafe URLs
    • Minor accessibility and UI text/layout refinements across auth screens
  • Bug Fixes

    • Consistent try/catch/finally flows for login, signup, forgot/reset-password with clearer network/validation messages
    • Quiz results preserved during login
    • Verification and reset emails now generate correct base URLs for links

✏️ Tip: You can customize this high-level summary in your review settings.

- Fix open redirect issues in login and signup
- Improve error handling and loading state safety
- Ensure safe base URL resolution for auth emails
- Fix date handling in token expiry helpers
- Remove neon-http transaction usage and debug logs
- Resolve Next.js 15 and strict TypeScript issues
@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 14, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit 6c7adb0
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/696833ef8ae4ec00083c8178
😎 Deploy Preview https://deploy-preview-143--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

Standardizes auth request handling with try/catch/finally, adds safe redirect validation and base URL resolution, exports/adjusts date utilities, and removes transaction wrappers around paired user+token DB operations.

Changes

Cohort / File(s) Summary
Frontend: Forgot/Reset flows
frontend/app/[locale]/forgot-password/page.tsx, frontend/app/[locale]/reset-password/page.tsx
Replaced unconditional fetches with try/catch/finally for network/error handling and loading cleanup. Added export const dynamic = "force-dynamic"; to reset-password page.
Frontend: Login & Signup
frontend/app/[locale]/login/page.tsx, frontend/app/[locale]/signup/page.tsx
Added isSafeRedirectUrl and returnTo param handling; wrapped submit flows in try/catch/finally; moved pending quiz-result submission into login success path; standardized encoded returnTo links; minor UI/accessibility formatting.
API: Email/Reset URL construction
frontend/app/api/auth/password-reset/route.ts, frontend/app/api/auth/resend-verification/route.ts, frontend/app/api/auth/signup/route.ts
Introduced resolveBaseUrl and derive baseUrl from headers/env; replaced direct origin usage when constructing verification/reset links.
API: Token operations (atomicity change)
frontend/app/api/auth/password-reset/confirm/route.ts, frontend/app/api/auth/verify-email/route.ts
Removed transaction wrappers around paired DB operations (user update + token delete); now executed sequentially (no single-transaction atomicity).
Lib: Auth utilities
frontend/lib/auth/email-verification.ts, frontend/lib/auth/password-reset.ts
Exported addHours(date, hours): Date and refactored to compute the result from the provided date; added explicit return types.
Lib: HTTP utilities
frontend/lib/http/getBaseUrl.ts
Added `export function resolveBaseUrl(options: { origin?: string

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Browser
  participant App as Frontend App
  participant API
  participant DB

  User->>Browser: Submit login form
  Browser->>App: onSubmit(login, returnTo)
  App->>App: validate returnTo via isSafeRedirectUrl
  App->>API: POST /api/auth/login (credentials)
  API->>DB: verify credentials
  DB-->>API: auth result
  API-->>App: 200 OK (user) / error
  alt login success
    App->>App: check sessionStorage for pending quiz
    alt pending quiz exists
      App->>API: POST /api/quiz/result
      API->>DB: store quiz result
      DB-->>API: stored
      API-->>App: quiz response
      App->>Browser: store/clear sessionStorage pending result
    end
    App->>Browser: redirect to safe returnTo or /<locale>/dashboard
  else login failure
    App->>Browser: show specific error (EMAIL_NOT_VERIFIED / invalid)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 I hopped through code with nimble paws,
Wrapped fetches safe from sudden flaws,
Redirects checked, base URLs set,
Tokens changed — no sweat, no fret,
A tiny rabbit cheers these auth-flaws-free laws! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: addressing reviewer feedback and hardening authentication flows across multiple auth pages and API routes.

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

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b27eaf6 and 6c7adb0.

📒 Files selected for processing (1)
  • frontend/lib/http/getBaseUrl.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/lib/http/getBaseUrl.ts

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@frontend/app/`[locale]/forgot-password/page.tsx:
- Around line 34-37: The catch block in forgot-password/page.tsx currently
injects the caught error into the user-facing message via setError; remove the
`${err}` from the string so the displayed message remains generic (e.g.,
"Network error. Please check your connection and try again.") and log the full
error internally for diagnostics (use console.error or your app logger inside
the same catch). Update the catch surrounding the async request in the
forgot-password page where setError is called to only show the generic message
and ensure the raw error is recorded to logs but not rendered in the UI.

In `@frontend/app/`[locale]/login/page.tsx:
- Around line 14-18: The isSafeRedirectUrl function currently allows
protocol-relative URLs like "//evil.com/path" because it only checks
startsWith("/") and for "://"; update isSafeRedirectUrl to explicitly reject
protocol-relative URLs by returning false if the url startsWith("//"), keeping
the existing checks (reject non-leading-slash and strings containing "://");
reference the function name isSafeRedirectUrl and mirror the same
protocol-relative guard used on the signup page to prevent open redirects.
- Around line 48-123: The outer try/finally around the login fetch has no catch,
so network errors will bubble uncaught; add a catch block after the try to
handle errors from the initial fetch (and any code before the finally), logging
the error and calling setErrorMessage with a user-friendly message and
setErrorCode(null) as appropriate; keep the existing finally that calls
setLoading(false). Target the block around the POST to "/api/auth/login" (the
code that awaits fetch and reads res.json()), and ensure you still perform the
pending quiz flow (getPendingQuizResult) only when the request succeeds.

In `@frontend/app/`[locale]/reset-password/page.tsx:
- Around line 1-3: The export const dynamic = "force-dynamic" in
frontend/app/[locale]/reset-password/page.tsx is ignored because the file is a
Client Component ("use client"); either remove the "use client" directive to
make page.tsx a Server Component and keep the dynamic export, or move the
dynamic export to a parent server layout or create a Server Component wrapper
(e.g., export default function ResetPasswordPage() { return
<ResetPasswordClient/> } with export const dynamic = "force-dynamic") and keep
the existing client-only UI in a separate ResetPasswordClient client component.

In `@frontend/app/api/auth/password-reset/confirm/route.ts`:
- Around line 60-67: The current code runs the password update
(db.update(users).set({ passwordHash }).where(eq(users.id, userId))) and token
deletion (db.delete(passwordResetTokens).where(eq(passwordResetTokens.token,
token))) separately, causing a race where a failed delete leaves a valid reset
token; reintroduce atomicity by wrapping both operations in a transaction (use
sql.transaction(...) or your Drizzle/sql transaction helper from
`@neondatabase/serverless`) so the update and delete commit together, and if that
driver support is unavailable then at minimum reorder the operations to delete
the passwordResetTokens token first and only update the user's passwordHash
after a successful delete, or add a clear code comment documenting the accepted
tradeoff.

In `@frontend/lib/http/getBaseUrl.ts`:
- Line 8: The fallback that builds the base URL currently hardcodes "http://"
using options.host; update the getBaseUrl logic to derive the scheme instead:
prefer an explicit options.scheme or an options.forwardedProto (from
x-forwarded-proto) if present, fall back to "https://" rather than "http://",
and then concatenate with options.host; adjust any references to options.host
and the getBaseUrl function to use this scheme selection so production
environments default to HTTPS or follow the forwarded proto header.
🧹 Nitpick comments (7)
frontend/lib/http/getBaseUrl.ts (1)

5-12: Consider validating the resulting URL.

The function trusts the origin and host inputs without validation. Malicious or malformed header values could produce invalid or unsafe URLs. A quick new URL(base) check would catch malformed inputs early.

Proposed validation
     const base =
         options.origin ||
         process.env.NEXT_PUBLIC_APP_URL ||
         (options.host ? `https://${options.host}` : null);

     if (!base) {
         throw new Error("Unable to determine base URL");
     }

+    try {
+        new URL(base);
+    } catch {
+        throw new Error("Invalid base URL");
+    }
+
     return base.replace(/\/$/, "");
frontend/app/api/auth/password-reset/route.ts (1)

52-56: Missing semicolons and potential unhandled exception.

Lines 52 and 56 are missing semicolons, which is inconsistent with the rest of the file. Additionally, resolveBaseUrl can throw if no base URL is determined—consider wrapping in try/catch to return a controlled error response.

Proposed fix
-    const h = await headers()
+    const h = await headers();
     const baseUrl = resolveBaseUrl({
         origin: h.get("origin"),
         host: h.get("host"),
-    })
+    });
frontend/app/api/auth/resend-verification/route.ts (1)

17-18: Minor: Extra blank lines.

These extra blank lines appear to be formatting noise. Consider removing for consistency.

frontend/lib/auth/password-reset.ts (1)

5-9: Duplicate addHours implementation.

This function is identical to the one in frontend/lib/auth/email-verification.ts. Consider extracting it to a shared utility (e.g., @/lib/utils/date.ts) to avoid duplication and ensure consistent behavior.

Suggested approach

Create a shared utility:

// frontend/lib/utils/date.ts
export function addHours(date: Date, hours: number): Date {
    const result = new Date(date.getTime());
    result.setHours(result.getHours() + hours);
    return result;
}

Then import from both password-reset.ts and email-verification.ts:

-export function addHours(date: Date, hours: number): Date {
-    const result = new Date(date.getTime());
-    result.setHours(result.getHours() + hours);
-    return result;
-}
+import { addHours } from "@/lib/utils/date";
frontend/lib/auth/email-verification.ts (1)

5-9: Duplicated addHours utility across files.

This exact implementation exists in frontend/lib/auth/password-reset.ts (lines 4-8). Consider extracting to a shared date utility module to avoid duplication.

Example shared utility location
// frontend/lib/utils/date.ts
export function addHours(date: Date, hours: number): Date {
    const result = new Date(date.getTime());
    result.setHours(result.getHours() + hours);
    return result;
}

Then import from both email-verification.ts and password-reset.ts.

frontend/app/api/auth/signup/route.ts (1)

10-11: Import organization suggestion.

Consider grouping Next.js imports together at the top. The headers import from next/headers is separated from the NextResponse import.

Optional reorganization
-import { NextResponse } from "next/server";
+import { NextResponse } from "next/server";
+import { headers } from "next/headers";
 import bcrypt from "bcryptjs";
 import { z } from "zod";
 import { eq } from "drizzle-orm";
 
 import { db } from "@/db";
 import { users } from "@/db/schema/users";
 import { createEmailVerificationToken } from "@/lib/auth/email-verification";
 import { sendVerificationEmail } from "@/lib/email/sendVerificationEmail";
-import { headers } from "next/headers";
 import { resolveBaseUrl } from "@/lib/http/getBaseUrl";
frontend/app/[locale]/signup/page.tsx (1)

10-19: Consider extracting isSafeRedirectUrl to a shared utility.

This helper is duplicated in login/page.tsx (and the login version is missing the startsWith("//") check). Extracting to a shared module (e.g., lib/auth/redirect.ts) would ensure consistent security logic across all auth pages.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb46d8c and cec4606.

📒 Files selected for processing (12)
  • frontend/app/[locale]/forgot-password/page.tsx
  • frontend/app/[locale]/login/page.tsx
  • frontend/app/[locale]/reset-password/page.tsx
  • frontend/app/[locale]/signup/page.tsx
  • frontend/app/api/auth/password-reset/confirm/route.ts
  • frontend/app/api/auth/password-reset/route.ts
  • frontend/app/api/auth/resend-verification/route.ts
  • frontend/app/api/auth/signup/route.ts
  • frontend/app/api/auth/verify-email/route.ts
  • frontend/lib/auth/email-verification.ts
  • frontend/lib/auth/password-reset.ts
  • frontend/lib/http/getBaseUrl.ts
🧰 Additional context used
🧬 Code graph analysis (9)
frontend/app/api/auth/password-reset/confirm/route.ts (2)
frontend/db/schema/users.ts (1)
  • users (10-41)
frontend/db/schema/passwordResetTokens.ts (1)
  • passwordResetTokens (3-18)
frontend/app/[locale]/login/page.tsx (1)
frontend/lib/quiz/guest-quiz.ts (2)
  • getPendingQuizResult (27-49)
  • clearPendingQuizResult (51-54)
frontend/lib/auth/password-reset.ts (1)
frontend/lib/auth/email-verification.ts (1)
  • addHours (5-9)
frontend/app/api/auth/verify-email/route.ts (3)
frontend/db/index.ts (1)
  • db (54-54)
frontend/db/schema/users.ts (1)
  • users (10-41)
frontend/db/schema/emailVerificationTokens.ts (1)
  • emailVerificationTokens (3-18)
frontend/app/api/auth/password-reset/route.ts (2)
frontend/lib/http/getBaseUrl.ts (1)
  • resolveBaseUrl (1-15)
frontend/lib/email/sendPasswordResetEmail.ts (1)
  • sendPasswordResetEmail (9-26)
frontend/app/[locale]/signup/page.tsx (1)
frontend/components/auth/OAuthButtons.tsx (1)
  • OAuthButtons (5-21)
frontend/lib/auth/email-verification.ts (1)
frontend/lib/auth/password-reset.ts (1)
  • addHours (5-9)
frontend/app/api/auth/signup/route.ts (2)
frontend/lib/http/getBaseUrl.ts (1)
  • resolveBaseUrl (1-15)
frontend/lib/email/sendVerificationEmail.ts (1)
  • sendVerificationEmail (9-25)
frontend/app/api/auth/resend-verification/route.ts (2)
frontend/lib/http/getBaseUrl.ts (1)
  • resolveBaseUrl (1-15)
frontend/lib/email/sendVerificationEmail.ts (1)
  • sendVerificationEmail (9-25)
🔇 Additional comments (20)
frontend/app/api/auth/password-reset/route.ts (1)

17-62: LGTM on the URL construction refactor.

The switch to resolveBaseUrl for constructing the reset URL is a good improvement for consistency across auth routes. The logic correctly prioritizes origin, then falls back to environment variable or host.

frontend/app/api/auth/resend-verification/route.ts (1)

59-67: LGTM on base URL resolution.

The refactor to use resolveBaseUrl is consistent with the pattern applied across other auth routes, improving maintainability and ensuring reliable URL construction across deployments.

frontend/lib/auth/password-reset.ts (1)

11-23: LGTM on token creation logic.

The createPasswordResetToken function correctly uses the immutable addHours helper and generates a secure random UUID for the token.

frontend/lib/auth/email-verification.ts (1)

11-21: LGTM!

Token generation and storage logic is correct. Uses crypto.randomUUID() for secure token generation and properly calculates expiration 24 hours from creation.

frontend/app/[locale]/forgot-password/page.tsx (1)

21-40: Good error handling pattern with try/catch/finally.

The structure correctly ensures setLoading(false) runs regardless of success or failure, and separates HTTP errors from network errors appropriately.

frontend/app/api/auth/verify-email/route.ts (1)

46-53: Non-atomic operations may leave orphan tokens on partial failure.

Removing the transaction wrapper means if the user update succeeds but the token deletion fails, the token remains in the database. While the user is correctly verified and the token would eventually expire, this could leave stale data.

If this is intentional due to neon-http transaction limitations (as mentioned in commit message), the risk is acceptable since:

  1. User verification state is the critical operation
  2. Orphan tokens expire naturally and can't cause harm

Please confirm this trade-off is acceptable for your database driver constraints.

frontend/app/api/auth/signup/route.ts (1)

67-75: LGTM! Robust base URL resolution.

The resolveBaseUrl utility correctly handles the origin/host fallback chain with NEXT_PUBLIC_APP_URL as a safe default. This ensures verification links work across different deployment environments.

frontend/app/[locale]/reset-password/page.tsx (1)

31-54: LGTM! Clean error handling with try/catch/finally.

The pattern correctly:

  • Distinguishes HTTP errors from network failures
  • Ensures loading state is always cleared
  • Avoids leaking error details to users (unlike the forgot-password page)
frontend/app/[locale]/login/page.tsx (6)

7-10: LGTM!

Imports are correctly structured and align with the quiz result handling logic added below.


24-26: LGTM!

Clean normalization that ensures returnTo is always a string, simplifying downstream usage.


73-114: LGTM!

The quiz result handling is well-structured:

  • Proper try/catch/finally ensures cleanup runs regardless of success/failure
  • Errors are logged but don't block the login flow
  • clearPendingQuizResult() in finally prevents stale data accumulation

116-120: LGTM!

Safe redirect pattern with appropriate fallback. The validation approach is correct (pending the protocol-relative URL fix noted above).


173-175: LGTM!

Proper dynamic aria-label for accessibility.


187-189: LGTM!

Correct use of encodeURIComponent to safely pass returnTo through the forgot-password flow.

frontend/app/[locale]/signup/page.tsx (6)

24-26: LGTM!

Consistent returnTo normalization pattern.


45-85: LGTM!

Well-structured error handling with try/catch/finally:

  • Network errors display user-friendly messages
  • API errors show server-provided error or fallback
  • Loading state consistently reset in finally

94-106: LGTM!

Good UX improvement—hiding OAuth options after signup initiation keeps the verification message prominent.


151-158: LGTM!

Simplified password input with minLength validation. Removing the toggle reduces complexity for the signup form.


123-125: LGTM!

Proper URL encoding for returnTo parameter.


184-186: LGTM!

Consistent returnTo encoding across all navigation links.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread frontend/app/[locale]/forgot-password/page.tsx Outdated
Comment thread frontend/app/[locale]/login/page.tsx
Comment thread frontend/app/[locale]/login/page.tsx
Comment thread frontend/app/[locale]/reset-password/page.tsx
Comment thread frontend/app/api/auth/password-reset/confirm/route.ts Outdated
Comment thread frontend/lib/http/getBaseUrl.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
frontend/app/[locale]/login/page.tsx (1)

138-150: Missing error handling in resendVerification is inconsistent with PR goals.

The PR objective states it wraps network requests in try/catch/finally to surface user-friendly error messages, but this function lacks that protection. If the fetch throws (network failure), users receive no feedback.

🐛 Proposed fix
   async function resendVerification() {
     if (!email) return;
 
-    await fetch("/api/auth/resend-verification", {
-      method: "POST",
-      headers: { "Content-Type": "application/json" },
-      body: JSON.stringify({ email }),
-    });
-
-    setVerificationSent(true);
-    setErrorCode(null);
-    setErrorMessage(null);
+    try {
+      await fetch("/api/auth/resend-verification", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({ email }),
+      });
+
+      setVerificationSent(true);
+      setErrorCode(null);
+      setErrorMessage(null);
+    } catch {
+      setErrorMessage(
+        "Failed to resend verification. Please try again."
+      );
+    }
   }
♻️ Duplicate comments (1)
frontend/lib/http/getBaseUrl.ts (1)

1-15: Overall structure is sound, but the http:// fallback concern remains.

The function correctly prioritizes origin over environment variable over host fallback, and properly strips trailing slashes. However, the hardcoded http:// on line 8 was flagged in a previous review and remains unaddressed—this could cause issues in production HTTPS environments.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cec4606 and b27eaf6.

📒 Files selected for processing (4)
  • frontend/app/[locale]/forgot-password/page.tsx
  • frontend/app/[locale]/login/page.tsx
  • frontend/app/api/auth/password-reset/confirm/route.ts
  • frontend/lib/http/getBaseUrl.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/[locale]/forgot-password/page.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/[locale]/login/page.tsx (1)
frontend/lib/quiz/guest-quiz.ts (2)
  • getPendingQuizResult (27-49)
  • clearPendingQuizResult (51-54)
🔇 Additional comments (4)
frontend/app/api/auth/password-reset/confirm/route.ts (1)

61-68: Good: Token deletion before password update mitigates the race condition.

The reordering correctly addresses the atomicity concern raised in previous reviews. By deleting the token first, even if the password update fails, the token cannot be reused—the user would simply need to request a new reset. This is the safer sequence given the Neon HTTP driver's transaction limitations.

Consider adding a brief comment explaining this deliberate ordering for future maintainers:

+    // Delete token first: if password update fails, user must request a new reset
+    // (preferred over leaving a reusable token if the update succeeds but delete fails)
     await db
         .delete(passwordResetTokens)
         .where(eq(passwordResetTokens.token, token));
frontend/app/[locale]/login/page.tsx (3)

14-19: LGTM! The open redirect vulnerability is now properly mitigated.

The isSafeRedirectUrl function correctly guards against:

  • Non-relative paths (line 15)
  • Protocol-relative URLs like //evil.com (line 16)
  • URLs with embedded protocols (line 17)

This addresses the concern from the previous review.


52-135: LGTM! Login flow now has proper error handling.

The try/catch/finally structure correctly:

  • Catches network errors and displays user-friendly feedback (lines 127-131)
  • Ensures loading state is always reset in the finally block (line 134)
  • Handles API response errors separately within the try block (lines 64-75)

This addresses the concern from the previous review about unhandled network errors.


77-119: Good error isolation for the pending quiz result flow.

The nested try/catch/finally correctly:

  • Isolates quiz submission failures from the main login flow
  • Ensures clearPendingQuizResult() is always called in the finally block
  • Logs errors for debugging without blocking the user's redirect

This prevents a failed quiz save from breaking the login experience.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@ViktorSvertoka ViktorSvertoka merged commit b41d093 into develop Jan 15, 2026
8 checks passed
@ViktorSvertoka ViktorSvertoka deleted the chore/auth-improvements branch January 15, 2026 05:43
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.

2 participants