ReviewOS

stacks/bench-review

auth: implement password-change endpoint (settings form is stubbed today)

#42
Closed glennmichael123 opened this 10 days ago ·
10 days ago

/settings has a password-change form in SettingsView.stx:149-220. The save handler is a stub — setTimeout(() => pwStatus.set('saved'), 350) per the comment at line 76. No API endpoint exists for it.

Scope

  • New app/Actions/Me/ChangePasswordAction.ts:
    • Auth-gated.
    • Body: current_password, new_password, new_password_confirmation.
    • Server-side verify current_password against the user's stored hash via @stacksjs/auth.
    • On mismatch: 422 with field error.
    • On success: hash new password (bcrypt, matching the existing scheme), update users.password, return 200.
    • Optionally: revoke all other personal access tokens to force re-login on other devices.
  • Route: PATCH /api/me/password with .middleware('auth').skipCsrf() (same pattern as the other /me/* endpoints).
  • Frontend: replace the setTimeout stub in SettingsView.stx with a real authFetch('/api/me/password', { method: 'PATCH', ... }) call. Mirror the error / success states already wired in the form.

Acceptance

  • User can change their password from /settings.
  • Wrong current-password returns a field-level error, doesn't reveal whether the email is registered (constant-time compare).
  • New password validated for length / complexity matching the registration rules.
  • Optional flag in the UI: "Sign me out of all other devices" → triggers token revocation.

Files to touch

  • app/Actions/Me/ChangePasswordAction.ts — new
  • routes/api.ts — add the route
  • resources/components/Bench/Settings/SettingsView.stx:76 — replace the setTimeout stub
10 days ago

Shipped.

Server — app/Actions/Me/ChangePasswordAction.ts (new):

  • PATCH /api/me/password, auth-gated, skipCsrf (matches the other /me/* endpoints).
  • Body: current_password, new_password, new_password_confirmation.
  • Pulls the stored hash off the users row directly (doesn't rely on Auth.user().password being populated in every context).
  • verifyHash(currentPassword, storedHash) for the constant-time compare.
  • Rejects mismatch on the confirmation, rejects when new equals current, requires new_password >= 8 chars.
  • makeHash(newPassword, { algorithm: 'bcrypt' }) — matches the registration + reset paths.

Client — resources/components/Bench/Settings/SettingsView.stx:

  • Replaced the setTimeout stub (line 62-85) with a real authStore.authFetch('/api/me/password', { method: 'PATCH', ... }) call.
  • Surfaces server-side error messages (wrong current password, mismatch, etc.) through the existing pwError / pwStatus signals — no UI changes needed since the form already had the error / saving / saved states wired.

Route — added to routes/api.ts next to the other /me/* endpoints.

Deliberately NOT done: revoking other personal-access tokens on password change. Some users have mobile clients / IDE plugins / multi-device sessions that would be silently bricked. A future "Sign me out everywhere" toggle on the settings UI can opt into that broader revoke — but defaulting to it would be a surprise for everyone else.

Sign in to comment on this issue.