Skip to content

fix(auth): Forgot Password link is dead — wire the full reset-password flow #151

@gkhngyk

Description

@gkhngyk

Problem

Password recovery is broken — the Forgot Password link on the sign-in page is a placeholder, not a working flow. A user who forgets their password today has no recovery path; they're locked out until an admin runs an SQL update by hand.

Audit:

Surface State
Sign-in link at web/src/components/auth/sign-in-form.tsx:89-94 <a href="#"> — clicking it does nothing
/forgot-password route Doesn't exist
/reset-password route Doesn't exist
supabase.auth.resetPasswordForEmail() call Nowhere in the codebase
Supabase Reset Password email template Default content; URL not pointed at our app (we never wired this)

Scope

Build the standard Supabase password-reset flow end-to-end.

Route 1 — request a reset

web/src/app/[locale]/(marketing)/forgot-password/page.tsx — single email input. On submit:

await supabase.auth.resetPasswordForEmail(email, {
  redirectTo: `${APP_URL}/auth/confirm?next=/reset-password`,
});

Always show a generic "If that email exists, we've sent a reset link" message regardless of whether the email is registered — prevents account enumeration.

Route 2 — set the new password

web/src/app/[locale]/(marketing)/reset-password/page.tsx — password + confirm inputs. On submit:

await supabase.auth.updateUser({ password: newPassword });

The session is already established by /auth/confirm (the same OTP-verify route that powers invite acceptance — see #129), so updateUser works directly.

Supabase email template

In Supabase Dashboard → Authentication → Emails → Reset Password template, switch the link href from {{ .ConfirmationURL }} to:

{{ .RedirectTo }}&token_hash={{ .TokenHash }}&type=recovery

Same shape as the Invite user template we rewired in #129. The /auth/confirm route already handles type=recovery via verifyOtp({ type, token_hash }).

Wire the link

In sign-in-form.tsx:89-94, replace href="#" with <Link href="/forgot-password">.

Acceptance criteria

  • Clicking Forgot Password on /sign-in navigates to /forgot-password
  • Submitting an email triggers resetPasswordForEmail and shows the generic "we sent a link if the account exists" message regardless of email validity
  • The email arrives via Resend (verified in Resend Logs)
  • Clicking the link in the email lands on /reset-password with an active session (no /sign-up detour)
  • Setting a new password actually changes it — sign out + sign back in with the new password succeeds
  • pnpm tsc --noEmit + yarn lint clean
  • No regression on the existing invite-accept flow at /invite/<token> (the /auth/confirm route is shared)

Notes / not a good first issue

This touches auth flows, the shared /auth/confirm route, Supabase email templates, and account-enumeration concerns (the "generic response on submit" rule above is important — don't leak which emails are registered). Someone picking it up should be comfortable with the verifyOtp pattern in #129.

Out of scope

  • Magic-link login (separate auth method, not what's broken here)
  • Password strength meter on the new-password input (existing forms don't have one either; add separately)
  • Localized email body — we only ship one locale right now

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions