feat: add password reset (forgot password) flow #4
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
There is currently no way for a user to reset their password. The
/loginpage has no "Forgot password?" link, no/forgot-passwordroute exists, and no/reset-passwordroute exists. If a user loses access to their account, they are fully locked out with no self-service recovery path.Root cause
The Better Auth server config (
packages/auth/src/index.ts) does not include the built-inforgotPassword/resetPasswordplugin, and no corresponding UI routes have been built.What needs to be implemented
1. Enable Better Auth's password reset plugin
In
packages/auth/src/index.ts, add theforgotPasswordplugin frombetter-auth/plugins. This plugin exposes two new API endpoints automatically via the existing/api/auth/$catch-all handler:POST /api/auth/forget-password— accepts an email, generates a signed reset token, and calls asendResetPasswordcallbackPOST /api/auth/reset-password— accepts the token + new password and updates the credential2. Add a password reset email template
In
packages/api/src/email.ts, add a newsendPasswordResetEmail(to: string, resetUrl: string)function following the existing Nodemailer + inline HTML pattern used bysendConfirmationEmail. The email should:resetUrl3. Add a
/forgot-passwordrouteNew file:
apps/web/src/routes/forgot-password.tsxA simple form that accepts an email address and calls
authClient.forgetPassword({ email, redirectTo: "/reset-password" }). Should show a confirmation message after submission (do not reveal whether the email exists — always show "if this email is registered, a link has been sent").4. Add a
/reset-passwordrouteNew file:
apps/web/src/routes/reset-password.tsxReads the
?token=query param from the URL (Better Auth appends this automatically to theredirectToURL). Renders a form with two fields (new password + confirm password). On submit callsauthClient.resetPassword({ token, newPassword }). On success redirect to/login.5. Add a "Forgot password?" link to the login page
In
apps/web/src/routes/login.tsx, add a small "Wachtwoord vergeten?" link below the password field that navigates to/forgot-password.Files to change
packages/auth/src/index.tsforgotPasswordplugin withsendResetPasswordcallbackpackages/api/src/email.tssendPasswordResetEmail()functionapps/web/src/routes/forgot-password.tsxapps/web/src/routes/reset-password.tsx?token=apps/web/src/routes/login.tsxNo database migrations are needed — Better Auth reuses the existing
verificationtable for reset tokens.Notes
SMTP_HOST,SMTP_USER,SMTP_PASS) is already in place viapackages/env/src/server.ts, so email delivery requires no new infrastructure.expiresInin the plugin options.authClientinapps/web/src/lib/auth-client.tswill need to includeforgotPasswordClient()frombetter-auth/client/pluginssoauthClient.forgetPassword()andauthClient.resetPassword()are typed correctly.Geïmplementeerd ✅
De wachtwoord-reset flow is volledig geïmplementeerd en gecommit (
dfc8ace).Wat er gedaan is
Server (
packages/auth/src/index.ts)sendResetPasswordcallback toegevoegd aan deemailAndPasswordoptie in de Better Auth config (dit is de correcte manier — er bestaat geenforgotPasswordplugin)SMTP_HOST/USER/PASS/PORT/FROMenv varsClient (
apps/web/src/lib/auth-client.ts)requestPasswordResetenresetPasswordzijn standaard beschikbaar op de auth clientNieuwe routes
/forgot-password— e-mailadres invullen, privacy-veilige bevestigingsboodschap/reset-password?token=...— nieuw wachtwoord instellen via token uit de e-mailRoute tree (
routeTree.gen.ts) handmatig bijgewerkt zodat TypeScript de nieuwe routes kent.Flow
/forgot-password/api/auth/reset-password/<token>?callbackURL=/reset-password/reset-password?token=<token>/loginNieuw probleem:
/reset-passwordgeeft 404 op productieWat er mis gaat
De reset-link in de e-mail stuurt de gebruiker naar:
De server antwoordt met de HTML van de app, maar de route wordt niet herkend — de pagina toont "Not Found". In de SSR-manifest die mee is gerenderd zijn
/reset-passworden/forgot-passwordvolledig afwezig.Oorzaak
De code is geïmplementeerd in branch
opencode/eager-wizard(commitdfc8ace) maar is nog niet gedeployed naar productie. De live build stamt van vóór deze wijzigingen. Zodra de branch gemerged wordt inmain(of welke branch de deploy triggert) en er een nieuwe deploy gedraaid wordt, zouden de routes beschikbaar moeten zijn.Wat te doen
opencode/eager-wizardinmain(of de deploy-branch)bun run deploy(via@kk/infra/ Alchemy)/forgot-passwordlaadt correct/reset-password?token=...laadt de formulierpagina (niet "Not Found")Opmerking over
routeTree.gen.tsHet bestand
apps/web/src/routeTree.gen.tsis handmatig bijgewerkt in de commit zodat TypeScript tevreden is. Bij de volgendebun devofbun buildwordt dit bestand automatisch overschreven door de TanStack Router Vite-plugin — dat is normaal en gewenst. De plugin herkent de nieuwe routebestanden (forgot-password.tsx,reset-password.tsx) en genereert het bestand opnieuw met de juiste inhoud.