Skip to content

feat: implement forgot password and reset password functionality#144

Merged
JoachimLK merged 3 commits intomainfrom
feat/forgot-password
Apr 16, 2026
Merged

feat: implement forgot password and reset password functionality#144
JoachimLK merged 3 commits intomainfrom
feat/forgot-password

Conversation

@JoachimLK
Copy link
Copy Markdown
Contributor

@JoachimLK JoachimLK commented Apr 16, 2026

Summary

This pull request introduces a complete password reset flow for users, including both "forgot password" and "reset password" pages, and adds a link to the "forgot password" page on the sign-in screen. The new pages provide user-friendly forms, error handling, and tracking, while following security best practices such as preventing email enumeration.

Password reset flow implementation:

  • Added a new forgot-password.vue page that allows users to request a password reset link by entering their email. The page includes form validation, error handling, success messaging, and tracking of user actions.
  • Added a new reset-password.vue page that lets users set a new password using a reset token. The page handles token validation, password validation, error and success messaging, and tracks relevant events.

Sign-in page update:

  • Added a "Forgot password?" link to the sign-in page (sign-in.vue), directing users to the new password reset flow.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features

    • Password recovery flow: request a reset by email and set a new password via secure reset link.
    • "Forgot password?" link added to the sign-in page.
  • Bug Fixes

    • Resumes: empty PDF uploads are now handled gracefully without errors.
  • Security

    • Improved server-side protections for external identity provider endpoints to reduce request-based abuse.

@railway-app
Copy link
Copy Markdown

railway-app Bot commented Apr 16, 2026

🚅 Deployed to the reqcore-pr-144 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Apr 16, 2026 at 6:32 pm

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-144 April 16, 2026 17:09 Destroyed
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Warning

Rate limit exceeded

@JoachimLK has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 28 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 55 minutes and 28 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e6fdcea-6237-4c72-864b-1deef65d339e

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0abd6 and 3f6a56b.

📒 Files selected for processing (1)
  • server/utils/auth.ts
📝 Walkthrough

Walkthrough

Adds a two-step password recovery flow (forgot-password and reset-password pages), a "Forgot password?" link on sign-in, SSRF protections for OIDC issuer/endpoint prefetched hosts, an empty-buffer guard in PDF parsing, and removal of the X-XSS-Protection header from global routeRules.

Changes

Cohort / File(s) Summary
New Auth Pages
app/pages/auth/forgot-password.vue, app/pages/auth/reset-password.vue
Adds guest-only pages with auth layout and noindex robots meta. Forgot-password collects email, calls authClient.requestPasswordReset, tracks page/submission events, and shows a generic success UI. Reset-password reads token from query, validates passwords (min 8, confirmation), calls authClient.resetPassword, tracks events, and handles token/errors.
Sign-in Link
app/pages/auth/sign-in.vue
Inserted a localized "Forgot password?" NuxtLink before the submit button; no other sign-in logic changed.
Nuxt Config
nuxt.config.ts
Removed the X-XSS-Protection: "1; mode=block" response header from the global nitro.routeRules["/**"].headers.
SSRF Mitigations (API)
server/api/sso/providers.post.ts
Added BLOCKED_ISSUER_HOSTNAMES and isBlockedIssuerUrl refinement to issuer validation schema to reject issuer URLs resolving to loopback, link-local, private, or otherwise blocked hostnames/IPs.
SSRF Mitigations (Utils)
server/utils/auth.ts
Introduced isBlockedHost helper and blocklist; prefetchOidcEndpointOrigins(issuerUrl) now validates and throws 422 for blocked/malformed hostnames before network requests.
Resume Parser Guard
server/utils/resume-parser.ts
Early-return null when buffer.length === 0 to avoid running pdfjs/pdf-parse on empty input.

Sequence Diagram

sequenceDiagram
    participant User
    participant Browser
    participant ForgotPage as Forgot Password Page
    participant ResetPage as Reset Password Page
    participant AuthClient as Auth Client
    participant Backend as Backend API
    participant Email as Email Service

    User->>Browser: Click "Forgot password?" on Sign-in
    Browser->>ForgotPage: Navigate to /auth/forgot-password
    ForgotPage->>AuthClient: requestPasswordReset(email, redirectTo)
    AuthClient->>Backend: POST /auth/forgot-password { email, redirectTo }
    Backend->>Email: Queue/send reset email with token link
    Backend-->>AuthClient: 200 OK
    AuthClient-->>ForgotPage: resolve
    ForgotPage-->>Browser: show generic success UI

    Email->>User: User clicks reset link (with token)
    User->>Browser: Navigate to /auth/reset-password?token=...
    Browser->>ResetPage: load page and track view
    User->>ResetPage: Submit new password
    ResetPage->>AuthClient: resetPassword({ newPassword, token })
    AuthClient->>Backend: POST /auth/reset-password { token, newPassword }
    Backend->>Backend: validate token, update password
    Backend-->>AuthClient: 200 OK
    AuthClient-->>ResetPage: resolve
    ResetPage-->>Browser: show success UI and sign-in link
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
Hop, hop — a form appears at dawn,
Email sent, then token drawn.
Reset the keys, the rabbit sings,
Safe hops back what morning brings.
✨🔐

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the summary, type of change, and validation sections from the template. However, the DCO section checkbox is not checked, indicating commits may not be signed off as required by the repository policy. Ensure all commits in this PR are signed off via 'git commit -s' and mark the DCO checkbox as completed before merging.
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and clearly summarizes the main change: implementing forgot password and reset password functionality, which aligns with the primary additions of two new auth pages and supporting UI changes.

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

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

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

@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.

🧹 Nitpick comments (1)
app/pages/auth/reset-password.vue (1)

27-74: Consider clearing password fields after successful reset.

The password values remain in the reactive refs after a successful reset. For defense-in-depth, consider clearing them to minimize the time sensitive data stays in memory.

🛡️ Optional: Clear passwords after success
     track("reset_password_completed");
     success.value = true;
     isLoading.value = false;
+    newPassword.value = "";
+    confirmPassword.value = "";
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/auth/reset-password.vue` around lines 27 - 74, The password refs
are left populated after a successful reset in handleResetPassword; after
setting success.value = true (and before finishing), clear the sensitive refs
(newPassword.value and confirmPassword.value) and any related token if desired
(token.value) to minimize sensitive data in memory, ensuring you update the same
function (handleResetPassword) and keep isLoading.value handling unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/pages/auth/reset-password.vue`:
- Around line 27-74: The password refs are left populated after a successful
reset in handleResetPassword; after setting success.value = true (and before
finishing), clear the sensitive refs (newPassword.value and
confirmPassword.value) and any related token if desired (token.value) to
minimize sensitive data in memory, ensuring you update the same function
(handleResetPassword) and keep isLoading.value handling unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c729041-002b-475d-8464-d75b5fe0ae21

📥 Commits

Reviewing files that changed from the base of the PR and between 753b37e and ad864ef.

📒 Files selected for processing (3)
  • app/pages/auth/forgot-password.vue
  • app/pages/auth/reset-password.vue
  • app/pages/auth/sign-in.vue

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-144 April 16, 2026 18:20 Destroyed
Comment thread server/utils/auth.ts Fixed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-144 April 16, 2026 18:29 Destroyed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
server/api/sso/providers.post.ts (1)

6-36: Code duplication with isBlockedHost in auth.ts — consider reusing.

This function duplicates isBlockedHost from server/utils/auth.ts with a subtle difference: line 34 only checks hostname.startsWith('fe80:') but the auth.ts version (line 52) also handles bracketed IPv6 notation [fe80:. This inconsistency could allow a bypass in the Zod validation layer.

Since prefetchOidcEndpointOrigins() already validates internally via isBlockedHost(), consider either:

  1. Exporting isBlockedHost from auth.ts and reusing it here
  2. Removing this duplicate validation and relying on the guard inside prefetchOidcEndpointOrigins()

If you keep both layers for defense-in-depth, at minimum add the missing bracket check:

🛡️ Quick fix for the missing bracket check
   if (hostname === '::1') return true
-  if (hostname.startsWith('fe80:')) return true
+  if (hostname.startsWith('fe80:') || hostname.startsWith('[fe80:')) return true
   return false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/sso/providers.post.ts` around lines 6 - 36, This duplicates the
SSRF hostname check in isBlockedHost (auth.ts); fix by importing and reusing
isBlockedHost inside isBlockedIssuerUrl (or remove this duplicate and rely on
prefetchOidcEndpointOrigins), otherwise make the duplicate consistent by adding
the missing IPv6 bracket check (e.g., also treat hostnames starting with
'[fe80:' as blocked) and ensure the same blocked set and IPv4 ranges logic as
isBlockedHost so validation behavior is identical to
prefetchOidcEndpointOrigins.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/auth/forgot-password.vue`:
- Around line 90-95: The error message container currently rendered when the
reactive property "error" is set should be made screen-reader announceable:
update the div that displays {{ error }} to include accessibility attributes
(e.g., role="alert" and aria-live="assertive", and optionally
aria-atomic="true") so assistive technologies immediately announce failures;
keep the conditional v-if="error" and the existing styling/classes intact while
adding these attributes to the error-rendering div.
- Around line 38-47: The current flow exposes whether an account exists because
it shows different UI for errors vs. success; update the forgot-password
submission handler so that regardless of result.error, thrown exceptions, or
success you always clear isLoading.value and set a generic success state/message
(do not assign to error.value or return early on result.error), logging/internal
errors can be recorded internally but the user-facing UI must always show the
same success message; apply the identical change to the reset-password handler
(the code that sets error.value and checks result.error/throws) so both handlers
always present a uniform success response to the client.

---

Nitpick comments:
In `@server/api/sso/providers.post.ts`:
- Around line 6-36: This duplicates the SSRF hostname check in isBlockedHost
(auth.ts); fix by importing and reusing isBlockedHost inside isBlockedIssuerUrl
(or remove this duplicate and rely on prefetchOidcEndpointOrigins), otherwise
make the duplicate consistent by adding the missing IPv6 bracket check (e.g.,
also treat hostnames starting with '[fe80:' as blocked) and ensure the same
blocked set and IPv4 ranges logic as isBlockedHost so validation behavior is
identical to prefetchOidcEndpointOrigins.
🪄 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: e6108bf9-b7e4-485f-acd8-d498dac2764c

📥 Commits

Reviewing files that changed from the base of the PR and between ad864ef and 8e0abd6.

📒 Files selected for processing (5)
  • app/pages/auth/forgot-password.vue
  • nuxt.config.ts
  • server/api/sso/providers.post.ts
  • server/utils/auth.ts
  • server/utils/resume-parser.ts
💤 Files with no reviewable changes (1)
  • nuxt.config.ts
✅ Files skipped from review due to trivial changes (1)
  • server/utils/resume-parser.ts

Comment on lines +38 to +47
if (result.error) {
error.value =
result.error.message ?? "Failed to send reset email. Please try again.";
isLoading.value = false;
return;
}
} catch (e: unknown) {
error.value =
e instanceof Error ? e.message : "Failed to send reset email. Please try again.";
isLoading.value = false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether forgot/reset flows emit user-specific messages that could enable enumeration.
# Expected: no "user not found / account does not exist" style messaging in this flow.

rg -n -C3 --iglob '*.{ts,js,vue}' \
'requestPasswordReset|forgot.?password|reset.?password|user not found|account.*exist|email.*exist|invalid email'

Repository: reqcore-inc/reqcore

Length of output: 6254


🏁 Script executed:

cat -n app/pages/auth/forgot-password.vue | sed -n '1,100p'

Repository: reqcore-inc/reqcore

Length of output: 3887


Behavioral enumeration risk: error vs. success states reveal account existence.

The code's control flow enables account enumeration regardless of message content. Errors (lines 38–42, 44–48) display an error panel and exit early, while success (lines 53–54) displays a success message. An attacker can infer whether an account exists by observing which UI state appears—this contradicts the anti-enumeration intent stated at line 53.

Treat all request outcomes (error, exception, success) as success to prevent behavioral leakage:

🔧 Proposed fix
 async function handleRequestReset() {
     error.value = "";

     if (!email.value) {
         error.value = "Email is required.";
         return;
     }

     isLoading.value = true;

     try {
         const result = await authClient.requestPasswordReset({
             email: email.value,
             redirectTo: `${window.location.origin}${localePath("/auth/reset-password")}`,
         });

         if (result.error) {
-            error.value =
-                result.error.message ?? "Failed to send reset email. Please try again.";
-            isLoading.value = false;
-            return;
+            // Treat all failures as success to prevent account enumeration.
         }
     } catch (e: unknown) {
-        error.value =
-            e instanceof Error ? e.message : "Failed to send reset email. Please try again.";
-        isLoading.value = false;
-        return;
+        // Treat all failures as success to prevent account enumeration.
     }

     track("forgot_password_submitted");

-    // Always show success to prevent email enumeration
     success.value = true;
     isLoading.value = false;
 }

Also check app/pages/auth/reset-password.vue (lines 58–69) for the same pattern.

📝 Committable suggestion

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

Suggested change
if (result.error) {
error.value =
result.error.message ?? "Failed to send reset email. Please try again.";
isLoading.value = false;
return;
}
} catch (e: unknown) {
error.value =
e instanceof Error ? e.message : "Failed to send reset email. Please try again.";
isLoading.value = false;
async function handleRequestReset() {
error.value = "";
if (!email.value) {
error.value = "Email is required.";
return;
}
isLoading.value = true;
try {
const result = await authClient.requestPasswordReset({
email: email.value,
redirectTo: `${window.location.origin}${localePath("/auth/reset-password")}`,
});
if (result.error) {
// Treat all failures as success to prevent account enumeration.
}
} catch (e: unknown) {
// Treat all failures as success to prevent account enumeration.
}
track("forgot_password_submitted");
success.value = true;
isLoading.value = false;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/auth/forgot-password.vue` around lines 38 - 47, The current flow
exposes whether an account exists because it shows different UI for errors vs.
success; update the forgot-password submission handler so that regardless of
result.error, thrown exceptions, or success you always clear isLoading.value and
set a generic success state/message (do not assign to error.value or return
early on result.error), logging/internal errors can be recorded internally but
the user-facing UI must always show the same success message; apply the
identical change to the reset-password handler (the code that sets error.value
and checks result.error/throws) so both handlers always present a uniform
success response to the client.

Comment on lines +90 to +95
<div
v-if="error"
class="rounded-md border border-danger-200 dark:border-danger-800 bg-danger-50 dark:bg-danger-950 p-3 text-sm text-danger-700 dark:text-danger-400"
>
{{ error }}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make error feedback screen-reader announceable.

The error block should be a live alert so failures are announced immediately.

♿ Proposed fix
             <div
                 v-if="error"
+                role="alert"
+                aria-live="assertive"
                 class="rounded-md border border-danger-200 dark:border-danger-800 bg-danger-50 dark:bg-danger-950 p-3 text-sm text-danger-700 dark:text-danger-400"
             >
                 {{ error }}
             </div>
📝 Committable suggestion

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

Suggested change
<div
v-if="error"
class="rounded-md border border-danger-200 dark:border-danger-800 bg-danger-50 dark:bg-danger-950 p-3 text-sm text-danger-700 dark:text-danger-400"
>
{{ error }}
</div>
<div
v-if="error"
role="alert"
aria-live="assertive"
class="rounded-md border border-danger-200 dark:border-danger-800 bg-danger-50 dark:bg-danger-950 p-3 text-sm text-danger-700 dark:text-danger-400"
>
{{ error }}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/auth/forgot-password.vue` around lines 90 - 95, The error message
container currently rendered when the reactive property "error" is set should be
made screen-reader announceable: update the div that displays {{ error }} to
include accessibility attributes (e.g., role="alert" and aria-live="assertive",
and optionally aria-atomic="true") so assistive technologies immediately
announce failures; keep the conditional v-if="error" and the existing
styling/classes intact while adding these attributes to the error-rendering div.

@JoachimLK JoachimLK merged commit aa00e89 into main Apr 16, 2026
9 checks passed
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.

1 participant