As a user I want to reset my forgotten password via email so I can regain access without contacting the admin #36
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?
Background
Passwords are set by the admin and users can change them on their profile page (#35). But if a user forgets their password before they change it, they are locked out and must ask the admin to intervene. A self-service reset flow via email removes that friction.
Depends on #35 — the
emailcolumn onapp_usersmust exist before this can be implemented.Desired behaviour
/forgot-password— a simple form with one field: email address/reset-password?token=...— a form with two fields: new password and confirmation/loginwith a success message/forgot-passwordImplementation notes
Backend
New Flyway migration — password reset token table:
New service method
UserService.requestPasswordReset(email):SecureRandom, 256-bit hex)PasswordResetTokenwithexpires_at = now() + 24hJavaMailSenderwith a link:${app.base-url}/reset-password?token=...MailException— if SMTP is not configured or delivery fails, log the error but return normally. The UI always shows "check your inbox" so SMTP misconfiguration does not leak information or produce a 500.New service method
UserService.resetPassword(token, newPassword):DomainException.notFoundif missingDomainException.conflictif invalidToken cleanup: A
@Scheduledjob runs nightly and deletes all tokens whereexpires_at < now()orused = true. This prevents the table from growing indefinitely.New endpoints (no authentication required — must be added to the Spring Security permit-list alongside
/api/auth/login):POST/api/auth/forgot-passwordPOST/api/auth/reset-passwordEmail configuration
All mail settings are externalised as environment variables:
docker-compose.ymlpassesMAIL_HOST,MAIL_USERNAME,MAIL_PASSWORD,MAIL_FROM,APP_BASE_URLas env vars so the same image works in both environments.Frontend
src/routes/forgot-password/— email form, always shows "check your inbox" after submitsrc/routes/reset-password/— reads?tokenfrom URL, shows new-password form; on success redirects to/loginwith a success message (no auto-login)de.json/en.json/es.jsonTesting
UserServiceTest:requestPasswordResetwith unknown email does nothing and does not throwrequestPasswordResetwith known email creates a token and callsJavaMailSenderrequestPasswordResetwhenJavaMailSenderthrowsMailExceptiondoes not propagate the exceptionrequestPasswordResetdeletes any existing token for the user before creating a new oneresetPasswordwith expired token throwsresetPasswordwith used token throwsresetPasswordwith valid token updates password hash, marks token used, invalidates sessions@WebMvcTest— both endpoints return 200 regardless of whether email matchesDependencies
User Journey
User arrives at the login page but can't remember their password. They click the "Forgot password?" link beneath the login form. On the next page they enter their email address and submit. The page always shows a neutral "check your inbox" confirmation — no hint about whether the email is registered.
They open their email, click the reset link, and land on a page with two fields: new password and confirmation. They submit and are redirected to the login page with a success message. They log in immediately with their new password.
If they click an old or already-used reset link, they see a clear error explaining the link has expired, with a link back to the forgot-password page to start over.
E2E Scenarios