Password Management in Plings-Web — Design Spec

Created: Thu 11 Jun 2026 16:18:30 CEST Document Version: 1.0 - Initial design spec Security Classification: Internal Technical Documentation Target Audience: Frontend Developers Author: Paul Wisén

Problem

Two gaps in password handling today:

  1. Password reset via e-mail does not lead to a “set new password” step. The reset link signs the user in directly (recovery session) and lands on the home page, with no way to actually set a new password. A complete set-new-password page already exists at /reset-password (src/pages/ResetPassword.tsx) but users never reach it, because:
    • resetPassword in src/contexts/AuthContext.tsx hardcodes redirectTo: 'https://plings.io/reset-password' (breaks local dev and preview deploys), and
    • the Supabase project’s Redirect URLs allowlist most likely does not include /reset-password, so Supabase falls back to the Site URL (home page).
  2. No way to change the password inside the app. The UserProfile dropdown has no account settings, and no settings page exists.

Decisions (from design discussion)

  • In-app password change lives in a dialog opened from the profile dropdown (no settings page yet).
  • No current-password requirement for the in-app change — users who signed up via e-mail verification may not have a usable password at all.
  • After a successful reset via e-mail link, the user stays signed in and is taken to the dashboard (today’s code signs the user out and forces a fresh login).

Design

1. Recovery flow fix

  • AuthContext.resetPassword: redirectTo becomes `${window.location.origin}/reset-password` so the flow works in local dev, preview deploys, and production.
  • The existing onAuthStateChange listener in AuthContext additionally handles the PASSWORD_RECOVERY event by navigating to /reset-password (via window.location.assign, only if not already there). This is the safety net: even if Supabase falls back to the Site URL, the user is forced into the set-password form.
  • Supabase dashboard configuration (manual step): add to Auth → URL Configuration → Redirect URLs:
    • https://plings.io/reset-password
    • http://localhost:8080/reset-password

2. ResetPassword page adjustments

  • On success: no sign-out; show success toast and navigate('/dashboard').
  • Form and validation otherwise unchanged.

3. Shared component: PasswordFields

  • New src/components/auth/PasswordFields.tsx: new-password + confirm fields plus the live requirements checklist (min 8 chars, uppercase, lowercase, number, special character). Exports validatePassword.
  • ResetPassword.tsx is refactored to use it, so the page and the dialog share one validation implementation.

4. New component: ChangePasswordDialog

  • New src/components/auth/ChangePasswordDialog.tsx: shadcn dialog wrapping PasswordFields, calls supabase.auth.updateUser({ password }).
  • New “Change password” menu item (key icon) in the UserProfile dropdown, between the e-mail row and Sign out.
  • Error handling: Supabase errors are shown inside the dialog (e.g. “New password should be different from the old password”); success shows a toast and closes the dialog.

5. Testing

Manual verification against the local dev server (no test runner exists in Plings-Web):

  1. Forgot password → e-mail → link → set new password → land signed-in on dashboard.
  2. Profile menu → Change password → new password works on next sign-in.
  3. Expired/invalid reset link shows an error and redirects home.

Future Work — Alternativ B: migrate to PKCE auth flow

Decided during design (2026-06-11): to be done later as its own effort.

Today the app uses Supabase’s implicit flow (tokens in the URL hash). Supabase’s recommended modern approach is PKCE (flowType: 'pkce' in the client config) with explicit code verification on the reset page (exchangeCodeForSession / verifyOtp with token_hash). Benefits: no tokens in URLs (safer with logging/history/referrers). Scope warning: switching affects all auth flows (sign-in, e-mail verification, password recovery) and needs end-to-end retesting of each — which is why it was split out of this work.