Skip to content

feat(auth): add zxcvbn-based password strength validation across auth flows#204

Open
NAYAMATVISION wants to merge 4 commits into
geturbackend:mainfrom
NAYAMATVISION:feat/password-strength-validation
Open

feat(auth): add zxcvbn-based password strength validation across auth flows#204
NAYAMATVISION wants to merge 4 commits into
geturbackend:mainfrom
NAYAMATVISION:feat/password-strength-validation

Conversation

@NAYAMATVISION
Copy link
Copy Markdown
Contributor

@NAYAMATVISION NAYAMATVISION commented May 25, 2026

Summary

This PR introduces password strength validation using zxcvbn across authentication flows.

Backend

  • Added reusable password strength validation utility

  • Enforced minimum password strength in:

    • Signup/Register
    • Reset Password
    • Change Password
  • Preserved existing validation and error handling patterns

Frontend

  • Added reusable PasswordStrengthMeter component
  • Real-time password strength analysis
  • User feedback and suggestions while typing
  • Disabled submission for weak passwords
  • Reduced unnecessary authentication API requests

Security Improvements

  • Penalizes passwords derived from:

    • User name
    • Email address
    • Email username/domain tokens
  • Prevents common personal-information-based passwords from being accepted

Testing

Verified:

  • Weak passwords are rejected
  • Strong passwords are accepted
  • Personal-information-based passwords are scored appropriately
  • Signup, password reset, and password update flows behave correctly
image image image image image

Summary by CodeRabbit

  • New Features

    • Password strength meter with color bar, score labels, and feedback shown on Signup, Reset Password, and Settings pages.
    • UI now prevents submission of weak passwords and disables submit buttons until strength is sufficient; inline "Too weak to submit" message displayed.
  • Bug Fixes

    • Server-side validation updated so weak passwords are rejected consistently.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

Warning

Review limit reached

@NAYAMATVISION, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 12 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a263155-e31b-4d63-939e-c439aa9da22a

📥 Commits

Reviewing files that changed from the base of the PR and between 94973ff and a37f588.

📒 Files selected for processing (1)
  • apps/dashboard-api/src/__tests__/auth.controller.test.js
📝 Walkthrough

Walkthrough

Adds zxcvbn-based password strength validation: a shared validator exported from common, Zod schema refinements, a PasswordStrengthMeter React component, and gating of signup, settings, and forgot-password flows plus a register-schema switch in the auth controller.

Changes

Password Strength Validation Feature

Layer / File(s) Summary
Core password strength validator
packages/common/package.json, packages/common/src/utils/passwordStrength.js, packages/common/src/index.js
Adds validatePasswordStrength using zxcvbn, enforces MIN_SCORE, exports the validator, and adds zxcvbn dependency.
Zod schema password strength enforcement
packages/common/src/utils/input.validation.js
Adds .superRefine(...) to multiple schemas to run validatePasswordStrength and emit custom Zod issues for weak passwords on password/newPassword fields.
PasswordStrengthMeter React component
apps/web-dashboard/package.json, apps/web-dashboard/src/components/PasswordStrengthMeter.jsx
Implements a reusable component that tokenizes user inputs, memoizes zxcvbn scoring, renders a colored strength bar, shows feedback, and reports score/sufficiency via callback.
ForgotPassword page password strength gating
apps/web-dashboard/src/pages/ForgotPassword.jsx
Wires PasswordStrengthMeter into reset flow, derives userInputs from email, tracks strength state, blocks weak-password submissions, and disables submit when password is present but too weak.
Settings page password strength enforcement
apps/web-dashboard/src/pages/Settings.jsx
Adds PasswordStrengthMeter to the password-change form, computes userInputs from user email, blocks weak passwords in the handler, and improves error parsing for failures.
Signup page password strength gating
apps/web-dashboard/src/pages/Signup.jsx
Replaces the password checklist with PasswordStrengthMeter, derives userInputs from name/email, enforces strength before account creation, and updates submit-button disabled logic.
Auth controller register schema switch
apps/dashboard-api/src/controllers/auth.controller.js
Switches registration request validation to use registerSchema imported from @urbackend/common.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels: NSOC'26, level-3

Suggested reviewers:

  • yash-pouranik

🐰 I nibble threats and hunt for weak string,
zxcvbn measures the strength that you bring,
Schemas and meters now guard every gate,
Stronger passwords dash threats — that's first-rate! 🥕🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: adding zxcvbn-based password strength validation across authentication flows, which is reflected throughout the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ 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: 5

🧹 Nitpick comments (1)
apps/web-dashboard/src/components/PasswordStrengthMeter.jsx (1)

4-4: 🏗️ Heavy lift

Avoid duplicating password policy thresholds in the UI.

MIN_SCORE is hardcoded locally while backend enforces its own threshold. This is a cross-layer contract and can drift. Please source the minimum score from a shared policy constant/config so client gating always matches server validation.

Also applies to: 34-34

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web-dashboard/src/components/PasswordStrengthMeter.jsx` at line 4, The
local constant MIN_SCORE in PasswordStrengthMeter.jsx duplicates the backend
policy; replace it by importing the shared policy value (e.g.,
PASSWORD_MIN_SCORE or passwordPolicy.minScore) from the shared config/module
used by the backend and use that instead anywhere MIN_SCORE is referenced
(including the check in the PasswordStrengthMeter component around line 34); if
import may be undefined at runtime, add a small fallback default and a clear
TODO to remove it once the shared policy is guaranteed available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web-dashboard/src/components/PasswordStrengthMeter.jsx`:
- Around line 45-96: Wrap the visual strength bar and textual status in
accessible semantics: add progress semantics to the inner strength bar div
(role="progressbar", aria-valuemin="0", aria-valuemax="100",
aria-valuenow={Math.round(barWidth)} and aria-valuetext={`${config.label}
(${Math.round(barWidth)}%)`}) and add a polite live region for updates (e.g.,
aria-live="polite" aria-atomic="true") around the score label/feedback block so
changes to config.label, the "Too weak to submit" message (when score <
MIN_SCORE) and feedback.warning/suggestions are announced; ensure unique content
uses existing symbols barWidth, config.label, MIN_SCORE, feedback to populate
ARIA attributes.

In `@apps/web-dashboard/src/pages/ForgotPassword.jsx`:
- Around line 102-105: The submit gating currently reads stale
passwordStrength.isStrongEnough (used in handleResetPassword and the submit
button disabled logic) which is updated asynchronously by PasswordStrengthMeter;
instead derive the strength synchronously from the current formData.newPassword
in the parent: add a local synchronous check (e.g.,
computeIsStrongEnoughFrom(formData.newPassword) using the same validation logic
as PasswordStrengthMeter) and use that boolean for both the early-return in
handleResetPassword and the disabled prop on the submit button so both places
use the up-to-date value from formData.newPassword rather than the
effect-updated passwordStrength.

In `@packages/common/src/utils/input.validation.js`:
- Around line 19-27: The loginSchema currently calls validatePasswordStrength
inside its .superRefine (see loginSchema, superRefine, validatePasswordStrength
and ctx.addIssue with path ["password"]), which rejects older but valid
passwords before authentication; remove the strength check from
loginSchema.superRefine so login only validates presence/format and does not
call validatePasswordStrength, and ensure validatePasswordStrength remains used
only in registration/change-password flows (e.g., signupSchema or changePassword
handlers) instead.
- Line 7: signupSchema and userSignupSchema currently allow weak passwords;
update both schemas to apply the same backend strength enforcement used in
password-change/reset flows by invoking validatePasswordStrength as the password
field's validator (same pattern used for change/reset flows). Locate
signupSchema and userSignupSchema and replace their plain password validation
with the custom validator call to validatePasswordStrength so registrations
enforce the same strength rules as passwordChange/passwordReset.

In `@packages/common/src/utils/passwordStrength.js`:
- Around line 12-23: The server-side validatePasswordStrength function currently
only accepts a single password and therefore cannot penalize passwords that
include user-specific tokens (name/email); update validatePasswordStrength to
accept an optional tokens parameter (e.g., tokens: string[] or null), validate
that tokens is an array of strings (default to [] if absent), and pass those
tokens into zxcvbn (call zxcvbn(password, tokens)) so zxcvbn can penalize
matches against user identifiers; keep the existing return semantics (return {
message: WEAK_PASSWORD_MESSAGE } when typeof password !== "string" ||
password.length === 0 or result.score < MIN_SCORE, otherwise return null).

---

Nitpick comments:
In `@apps/web-dashboard/src/components/PasswordStrengthMeter.jsx`:
- Line 4: The local constant MIN_SCORE in PasswordStrengthMeter.jsx duplicates
the backend policy; replace it by importing the shared policy value (e.g.,
PASSWORD_MIN_SCORE or passwordPolicy.minScore) from the shared config/module
used by the backend and use that instead anywhere MIN_SCORE is referenced
(including the check in the PasswordStrengthMeter component around line 34); if
import may be undefined at runtime, add a small fallback default and a clear
TODO to remove it once the shared policy is guaranteed available.
🪄 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: 68b43af3-d178-455e-89ff-2d1b9e0336b2

📥 Commits

Reviewing files that changed from the base of the PR and between efe95a8 and 3542751.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • apps/web-dashboard/package.json
  • apps/web-dashboard/src/components/PasswordStrengthMeter.jsx
  • apps/web-dashboard/src/pages/ForgotPassword.jsx
  • apps/web-dashboard/src/pages/Settings.jsx
  • apps/web-dashboard/src/pages/Signup.jsx
  • packages/common/package.json
  • packages/common/src/index.js
  • packages/common/src/utils/input.validation.js
  • packages/common/src/utils/passwordStrength.js

Comment on lines +45 to +96
<div style={{ marginTop: '0.5rem' }}>
{/* Strength bar */}
<div style={{
height: '4px',
borderRadius: '2px',
background: 'var(--color-border)',
overflow: 'hidden',
marginBottom: '0.4rem',
}}>
<div style={{
height: '100%',
width: `${barWidth}%`,
background: config.color,
borderRadius: '2px',
transition: 'width 0.3s ease, background 0.3s ease',
}} />
</div>

{/* Score label */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: feedback.warning || feedback.suggestions?.length ? '0.4rem' : 0,
}}>
<span style={{ fontSize: '0.78rem', color: config.color, fontWeight: 600 }}>
{config.label}
</span>
{score < MIN_SCORE && (
<span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>
Too weak to submit
</span>
)}
</div>

{/* Warning */}
{feedback.warning && (
<p style={{ fontSize: '0.78rem', color: 'var(--color-text-muted)', margin: '0 0 0.25rem' }}>
{feedback.warning}
</p>
)}

{/* Suggestions */}
{feedback.suggestions?.length > 0 && (
<ul style={{ margin: 0, padding: '0 0 0 1rem' }}>
{feedback.suggestions.map((s) => (
<li key={s} style={{ fontSize: '0.78rem', color: 'var(--color-text-muted)', marginBottom: '0.15rem' }}>
{s}
</li>
))}
</ul>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add ARIA semantics for live password-strength feedback.

The meter and “Too weak to submit” feedback are currently visual-only. Add progress semantics and a polite live region so assistive tech announces strength changes.

♿ Suggested patch
-  return (
-    <div style={{ marginTop: '0.5rem' }}>
+  return (
+    <div style={{ marginTop: '0.5rem' }} aria-live="polite">
       {/* Strength bar */}
-      <div style={{
+      <div
+        role="progressbar"
+        aria-label="Password strength"
+        aria-valuemin={0}
+        aria-valuemax={4}
+        aria-valuenow={score}
+        aria-valuetext={config.label}
+        style={{
         height: '4px',
         borderRadius: '2px',
         background: 'var(--color-border)',
         overflow: 'hidden',
         marginBottom: '0.4rem',
-      }}>
+      }}>
         <div style={{
           height: '100%',
           width: `${barWidth}%`,
           background: config.color,
           borderRadius: '2px',
           transition: 'width 0.3s ease, background 0.3s ease',
         }} />
       </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web-dashboard/src/components/PasswordStrengthMeter.jsx` around lines 45
- 96, Wrap the visual strength bar and textual status in accessible semantics:
add progress semantics to the inner strength bar div (role="progressbar",
aria-valuemin="0", aria-valuemax="100", aria-valuenow={Math.round(barWidth)} and
aria-valuetext={`${config.label} (${Math.round(barWidth)}%)`}) and add a polite
live region for updates (e.g., aria-live="polite" aria-atomic="true") around the
score label/feedback block so changes to config.label, the "Too weak to submit"
message (when score < MIN_SCORE) and feedback.warning/suggestions are announced;
ensure unique content uses existing symbols barWidth, config.label, MIN_SCORE,
feedback to populate ARIA attributes.

Comment on lines +102 to +105
if (!passwordStrength.isStrongEnough) {
toast.error('Please choose a stronger password.');
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In React 19, does state updated from a child component's useEffect run after render/paint such that parent submit guards can observe the previous value for one render?

💡 Result:

Short answer: No—React doesn’t let a child component’s passive effect (useEffect) “race” with the parent’s render/paint such that a parent event handler (like an onSubmit) observes the previous state for one render. However, state updates scheduled from effects are not guaranteed to be applied before paint in all circumstances, so you also shouldn’t rely on useEffect timing for “next render” ordering unless you structure code around explicit user events or use layout effects when you must block painting. What the React docs guarantee / imply: - useEffect callbacks are deferred relative to the browser paint in the general case, and are run after layout and paint via a deferred event [1]. - React guarantees that even when effects are deferred until after the browser has painted, React will flush a previous render’s effects before starting a new update (so updates from the effect can’t be missed or applied “after” a newer render has begun) [2]. What that means for your scenario (child useEffect updates state used by a parent submit guard): - If the submit guard logic runs in response to a user interaction (e.g., the onSubmit handler), React will process updates such that the “next” UI/event system view isn’t allowed to “skip” the update that was scheduled earlier; useEffect timing is about when the effect runs, not about producing a one-render-old observable state to an unrelated later user event [1][2]. - Also, React’s own docs note that timing can differ for certain cases (e.g., effects caused by discrete user input or flushSync may be processed differently, and it’s also not guaranteed that useEffect always runs after paint) [1][3]. So while you typically won’t see a parent submit guard observe a previous value “for one render” due to passive-effect updates, you should not design the app assuming a strict paint-phase ordering across all scheduling situations. If you need the parent guard to observe the child-driven state change immediately before a user event: - Don’t depend on child useEffect for that; instead, update the parent state in the same interaction path (e.g., lift state up and call the parent’s state update directly, or call setState in an event handler). This avoids the asynchronous/deferred nature of passive effects [1][2]. - If you truly must block paint and ensure ordering with layout, useLayoutEffect is the correct tool (but note it has different performance/ordering semantics and can effectively make passive effects flush earlier) [4][2][5]. Conclusion: - In React 19, a passive child useEffect-driven state update is not expected to be applied “after render/paint” in a way that causes the parent’s next submit guard to see the previous value for exactly one render. React flushes previous effects before starting a new update [2]. - But you should avoid assuming a rigid “after paint” timing relationship for correctness, because React documentation and discussions note that effect timing can vary in special cases [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web-dashboard/src/pages/ForgotPassword.jsx"

# Show the relevant sections around the mentioned line ranges
nl -ba "$FILE" | sed -n '70,140p'
nl -ba "$FILE" | sed -n '190,260p'

Repository: geturbackend/urBackend

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web-dashboard/src/pages/ForgotPassword.jsx"

# Print with line numbers (avoid nl since it's missing)
echo "== $FILE : lines ~70-140 =="
cat -n "$FILE" | sed -n '70,140p'

echo
echo "== $FILE : lines ~190-260 =="
cat -n "$FILE" | sed -n '190,260p'

Repository: geturbackend/urBackend

Length of output: 5865


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where PasswordStrengthMeter is defined/implemented
rg -n "function PasswordStrengthMeter|const PasswordStrengthMeter|PasswordStrengthMeter" apps/web-dashboard/src -S

# 2) Find the prop name `onStrengthChange` usage
rg -n "onStrengthChange" apps/web-dashboard/src -S

# 3) Inspect how `handleResetPassword` is connected (form onSubmit / button type submit)
rg -n "handleResetPassword|onSubmit=\{handleResetPassword\}|type=\"submit\"" apps/web-dashboard/src/pages/ForgotPassword.jsx -n

Repository: geturbackend/urBackend

Length of output: 2041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE1="apps/web-dashboard/src/components/PasswordStrengthMeter.jsx"
FILE2="apps/web-dashboard/src/pages/ForgotPassword.jsx"

echo "== $FILE1 : lines 1-140 =="
cat -n "$FILE1" | sed -n '1,160p'

echo
echo "== $FILE2 : lines 1-120 =="
cat -n "$FILE2" | sed -n '1,140p'

echo
echo "== $FILE2 : around form onSubmit + submit button area =="
cat -n "$FILE2" | sed -n '140,320p'

Repository: geturbackend/urBackend

Length of output: 15117


Avoid effect-lagged password strength in reset submit gating

handleResetPassword (102-105) and the reset submit button disabled logic (252-256) depend on passwordStrength.isStrongEnough, but passwordStrength is updated via PasswordStrengthMeter using a useEffect (wiring at 218-222). If the user submits right after changing the password, this state can be temporarily out of date. Derive isStrongEnough directly from the current formData.newPassword in the parent (or otherwise ensure synchronous/ordered gating) for both the submit guard and button disabled state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web-dashboard/src/pages/ForgotPassword.jsx` around lines 102 - 105, The
submit gating currently reads stale passwordStrength.isStrongEnough (used in
handleResetPassword and the submit button disabled logic) which is updated
asynchronously by PasswordStrengthMeter; instead derive the strength
synchronously from the current formData.newPassword in the parent: add a local
synchronous check (e.g., computeIsStrongEnoughFrom(formData.newPassword) using
the same validation logic as PasswordStrengthMeter) and use that boolean for
both the early-return in handleResetPassword and the disabled prop on the submit
button so both places use the up-to-date value from formData.newPassword rather
than the effect-updated passwordStrength.

Comment thread packages/common/src/utils/input.validation.js
Comment thread packages/common/src/utils/input.validation.js
Comment on lines +12 to +23
function validatePasswordStrength(password) {
if (typeof password !== "string" || password.length === 0) {
return { message: WEAK_PASSWORD_MESSAGE };
}

const result = zxcvbn(password);

if (result.score < MIN_SCORE) {
return { message: WEAK_PASSWORD_MESSAGE };
}

return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add contextual token support to backend strength validation.

Line 12 currently accepts only password, so server-side validation cannot penalize passwords derived from user identifiers (e.g., name/email tokens). That leaves a bypass gap if client checks are skipped.

🔧 Proposed change
-function validatePasswordStrength(password) {
+function validatePasswordStrength(password, userInputs = []) {
   if (typeof password !== "string" || password.length === 0) {
     return { message: WEAK_PASSWORD_MESSAGE };
   }

-  const result = zxcvbn(password);
+  const result = zxcvbn(password, Array.isArray(userInputs) ? userInputs : []);

   if (result.score < MIN_SCORE) {
     return { message: WEAK_PASSWORD_MESSAGE };
   }

   return null;
 }
📝 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
function validatePasswordStrength(password) {
if (typeof password !== "string" || password.length === 0) {
return { message: WEAK_PASSWORD_MESSAGE };
}
const result = zxcvbn(password);
if (result.score < MIN_SCORE) {
return { message: WEAK_PASSWORD_MESSAGE };
}
return null;
function validatePasswordStrength(password, userInputs = []) {
if (typeof password !== "string" || password.length === 0) {
return { message: WEAK_PASSWORD_MESSAGE };
}
const result = zxcvbn(password, Array.isArray(userInputs) ? userInputs : []);
if (result.score < MIN_SCORE) {
return { message: WEAK_PASSWORD_MESSAGE };
}
return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/common/src/utils/passwordStrength.js` around lines 12 - 23, The
server-side validatePasswordStrength function currently only accepts a single
password and therefore cannot penalize passwords that include user-specific
tokens (name/email); update validatePasswordStrength to accept an optional
tokens parameter (e.g., tokens: string[] or null), validate that tokens is an
array of strings (default to [] if absent), and pass those tokens into zxcvbn
(call zxcvbn(password, tokens)) so zxcvbn can penalize matches against user
identifiers; keep the existing return semantics (return { message:
WEAK_PASSWORD_MESSAGE } when typeof password !== "string" || password.length ===
0 or result.score < MIN_SCORE, otherwise return null).

@yash-pouranik
Copy link
Copy Markdown
Collaborator

@NAYAMATVISION
2 comments are still not resolved?

@NAYAMATVISION
Copy link
Copy Markdown
Contributor Author

Thanks for pointing this out. The login/signup schema issue has been addressed in commit 1806467 and the corresponding review comment is now outdated.

I'm reviewing the remaining comments:

  1. Backend contextual token support for zxcvbn.
  2. ForgotPassword submit-gating observation.

I'll either address them or provide justification before marking the review thread resolved.

@NAYAMATVISION
Copy link
Copy Markdown
Contributor Author

Thanks for pointing that out.

While reviewing the remaining comments, I found an additional backend enforcement gap: the dashboard registration endpoint was still validating with loginSchema, which meant password-strength validation could be bypassed through direct API requests.

I've addressed this by introducing a dedicated registerSchema with password-strength validation and updating the dashboard registration flow to use it. Login validation remains unchanged, so existing users are not affected.

The remaining review comments have been reviewed, and the backend enforcement is now applied consistently across registration, password reset, and password change flows.

@NAYAMATVISION
Copy link
Copy Markdown
Contributor Author

The failing dashboard-api test was caused by the Jest mock for @urbackend/common not including the newly introduced registerSchema.

Production exports were correct; the mock returned undefined for registerSchema, causing registerSchema.parse() to throw during tests.

I've updated the mock and verified the auth controller test suite passes locally again.

1 similar comment
@NAYAMATVISION
Copy link
Copy Markdown
Contributor Author

The failing dashboard-api test was caused by the Jest mock for @urbackend/common not including the newly introduced registerSchema.

Production exports were correct; the mock returned undefined for registerSchema, causing registerSchema.parse() to throw during tests.

I've updated the mock and verified the auth controller test suite passes locally again.

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/dashboard-api/src/controllers/auth.controller.js`:
- Line 11: The runtime error means registerSchema is not actually exported from
the common package; inspect packages/common/src/utils/input.validation.js to
ensure registerSchema is defined, confirm packages/common/src/index.js
re-exports registerSchema (search for "registerSchema" in that index), and if
missing add the explicit export so module.exports.registerSchema (or export {
registerSchema }) is present; after fixing exports, rebuild the common package
and reinstall/rebuild apps/dashboard-api so it picks up the updated bundle
(ensure the controller import in auth.controller.js still points to the correct
package entry that now exports registerSchema).
🪄 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: 585fbe5e-ef5f-4819-b6ad-bcee7990c6dc

📥 Commits

Reviewing files that changed from the base of the PR and between 3542751 and 94973ff.

📒 Files selected for processing (3)
  • apps/dashboard-api/src/controllers/auth.controller.js
  • packages/common/src/index.js
  • packages/common/src/utils/input.validation.js

const crypto = require("crypto");
const {
loginSchema,
registerSchema,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

CRITICAL: registerSchema is undefined at runtime.

The pipeline failure indicates that registerSchema.parse throws "Cannot read properties of undefined (reading 'parse')", meaning registerSchema itself is undefined when this code executes. While the import statement on line 11 looks syntactically correct, the export chain from @urbackend/common appears broken at runtime.

Run the following script to verify the export chain:

#!/bin/bash
# Verify registerSchema is properly exported from `@urbackend/common`

echo "=== Checking input.validation.js export ==="
rg -n "module\.exports\.registerSchema" packages/common/src/utils/input.validation.js

echo -e "\n=== Checking index.js import from input.validation ==="
rg -n "registerSchema" packages/common/src/index.js

echo -e "\n=== Checking if common package needs rebuild ==="
# Check if there's a build script
cat packages/common/package.json | jq -r '.scripts.build // "No build script found"'

echo -e "\n=== Verifying the export is reachable ==="
# Try to require and inspect the export in a Node context
node -e "
try {
  const common = require('./packages/common/src/index.js');
  if (common.registerSchema === undefined) {
    console.error('ERROR: registerSchema is undefined in common exports');
    process.exit(1);
  }
  console.log('✓ registerSchema is exported and defined');
  console.log('✓ Type:', typeof common.registerSchema);
} catch (err) {
  console.error('ERROR loading common:', err.message);
  process.exit(1);
}
"

If the script confirms the export exists, the dashboard-api package may need to reinstall dependencies or rebuild to pick up the changes:

#!/bin/bash
cd apps/dashboard-api
npm install
# or if using a monorepo tool:
# npm run build --workspace=`@urbackend/common`

Also applies to: 278-278

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard-api/src/controllers/auth.controller.js` at line 11, The
runtime error means registerSchema is not actually exported from the common
package; inspect packages/common/src/utils/input.validation.js to ensure
registerSchema is defined, confirm packages/common/src/index.js re-exports
registerSchema (search for "registerSchema" in that index), and if missing add
the explicit export so module.exports.registerSchema (or export { registerSchema
}) is present; after fixing exports, rebuild the common package and
reinstall/rebuild apps/dashboard-api so it picks up the updated bundle (ensure
the controller import in auth.controller.js still points to the correct package
entry that now exports registerSchema).

@NAYAMATVISION
Copy link
Copy Markdown
Contributor Author

NAYAMATVISION commented May 26, 2026 via email

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.

feat(auth): enforce strong password validation for authentication flows

2 participants