feat: password reset via email (#36) #49

Merged
marcel merged 7 commits from feat/36-password-reset into main 2026-03-23 10:13:56 +01:00
Owner

Summary

  • Backend: POST /api/auth/forgot-password and POST /api/auth/reset-password — both unauthenticated. Token stored in new password_reset_tokens table (Flyway V8). Nightly cleanup via @Scheduled. If no SMTP is configured, logs a warning and silently skips sending.
  • Frontend: /forgot-password (email form → success banner) and /reset-password?token=… (new-password form). "Passwort vergessen?" link added to the login page.
  • Config: Mail settings read from MAIL_HOST / MAIL_PORT / MAIL_USERNAME / MAIL_PASSWORD / APP_MAIL_FROM env vars — all optional with safe defaults.
  • E2E: password-reset.spec.ts — 4 of 5 tests pass locally; the full reset flow test requires --spring.profiles.active=e2e (active in CI via AuthE2EController helper endpoint).

Test plan

  • All backend unit tests pass (./mvnw test)
  • Frontend lints and type-checks clean (npm run lint && npm run check)
  • /forgot-password renders without auth, shows success banner for any email
  • /reset-password?token=invalid shows "Der Link ist ungültig oder abgelaufen."
  • Password mismatch on reset form shows validation error
  • Full reset flow works in CI (e2e profile)
  • With MAIL_HOST unset: backend starts, no health failures, warning logged instead of email sent

🤖 Generated with Claude Code

## Summary - **Backend**: `POST /api/auth/forgot-password` and `POST /api/auth/reset-password` — both unauthenticated. Token stored in new `password_reset_tokens` table (Flyway V8). Nightly cleanup via `@Scheduled`. If no SMTP is configured, logs a warning and silently skips sending. - **Frontend**: `/forgot-password` (email form → success banner) and `/reset-password?token=…` (new-password form). "Passwort vergessen?" link added to the login page. - **Config**: Mail settings read from `MAIL_HOST` / `MAIL_PORT` / `MAIL_USERNAME` / `MAIL_PASSWORD` / `APP_MAIL_FROM` env vars — all optional with safe defaults. - **E2E**: `password-reset.spec.ts` — 4 of 5 tests pass locally; the full reset flow test requires `--spring.profiles.active=e2e` (active in CI via `AuthE2EController` helper endpoint). ## Test plan - [ ] All backend unit tests pass (`./mvnw test`) - [ ] Frontend lints and type-checks clean (`npm run lint && npm run check`) - [ ] `/forgot-password` renders without auth, shows success banner for any email - [ ] `/reset-password?token=invalid` shows "Der Link ist ungültig oder abgelaufen." - [ ] Password mismatch on reset form shows validation error - [ ] Full reset flow works in CI (e2e profile) - [ ] With `MAIL_HOST` unset: backend starts, no health failures, warning logged instead of email sent 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel changed target branch from feat/35-profile-page to main 2026-03-23 07:55:22 +01:00
marcel added 2 commits 2026-03-23 07:55:22 +01:00
- Add PasswordResetToken entity, repository (Flyway V8 migration)
- PasswordResetService: token generation, validation, nightly cleanup
- AuthController: POST /api/auth/forgot-password and /api/auth/reset-password (both permitAll)
- AuthE2EController (@Profile("e2e")): GET /api/auth/reset-token-for-test for CI testing
- spring-boot-starter-mail dependency; JavaMailSender optional (@Autowired required=false)
- mail health indicator disabled; mail config via MAIL_HOST/PORT/USERNAME/PASSWORD env vars
- 5 unit tests written TDD-style (all pass)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat(frontend): add forgot-password and reset-password pages
Some checks failed
CI / Unit & Component Tests (push) Successful in 2m7s
CI / Backend Unit Tests (push) Successful in 2m3s
CI / E2E Tests (push) Failing after 14m54s
CI / Unit & Component Tests (pull_request) Successful in 2m4s
CI / E2E Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
908221f04d
- /forgot-password: email form → sends POST /api/auth/forgot-password → success banner
- /reset-password: password form reads token from URL → sends POST /api/auth/reset-password
- Login page: add "Passwort vergessen?" link
- hooks.server.ts: add /forgot-password and /reset-password to PUBLIC_PATHS; skip auth
  injection for public auth API endpoints
- errors.ts: add INVALID_RESET_TOKEN error code
- i18n: add all new message keys in de/en/es
- playwright.config.ts: use E2E_BASE_URL for webServer check URL (allows reusing docker
  dev server at port 5173 locally)
- ci.yml: pass E2E_BACKEND_URL=http://localhost:8080 to E2E test step
- e2e/password-reset.spec.ts: 5 tests (4 pass locally, full flow requires e2e profile in CI)
- Regenerated OpenAPI types including new /api/auth/* endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-23 08:46:28 +01:00
fix(e2e): use username check instead of count() for admin user creation
Some checks failed
CI / Unit & Component Tests (push) Successful in 2m3s
CI / Backend Unit Tests (push) Successful in 2m5s
CI / E2E Tests (push) Has started running
CI / Unit & Component Tests (pull_request) Successful in 2m5s
CI / Backend Unit Tests (pull_request) Successful in 1m56s
CI / E2E Tests (pull_request) Failing after 18m40s
b9aff799fa
When the e2e profile is active, initE2EData (which creates a reader user)
can run before initAdminUser. The old count() == 0 guard then skips admin
creation entirely, causing every login test to fail with 401.

Switch to findByUsername(adminUsername).isEmpty() so the admin is created
regardless of which CommandLineRunner runs first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel reviewed 2026-03-23 09:02:17 +01:00
marcel added 1 commit 2026-03-23 09:10:42 +01:00
feat(dev): add Mailpit mail catcher to docker-compose
Some checks failed
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m6s
CI / Backend Unit Tests (pull_request) Successful in 2m7s
CI / E2E Tests (pull_request) Has been cancelled
c18cdbfac1
Adds a Mailpit container that catches all outgoing emails locally so
password reset links can be tested without a real SMTP server.

- Backend defaults to MAIL_HOST=mailpit / MAIL_PORT=1025 in compose
- SMTP auth and STARTTLS disabled for Mailpit (no credentials needed)
- Web inbox available at http://localhost:8025
- Production SMTP still works by overriding MAIL_HOST, MAIL_PORT,
  MAIL_USERNAME, MAIL_SMTP_AUTH, and MAIL_STARTTLS_ENABLE in .env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-23 09:21:03 +01:00
docs: add mail configuration guide
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
88e3fb32b3
Covers dev (Mailpit), production SMTP, all env vars with defaults,
common provider settings, and how to disable mail entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-23 09:28:22 +01:00
fix(frontend): hide nav header on forgot-password and reset-password routes
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
17db73d900
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-23 09:48:24 +01:00
fix(e2e): wait for hydration before clicking nav dropdown in logout test
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Successful in 2m8s
CI / Backend Unit Tests (push) Successful in 2m10s
CI / E2E Tests (push) Successful in 20m18s
43defa41c4
waitForURL('/') resolves as soon as the URL changes but before SvelteKit
finishes hydrating — the avatar button's onclick is not yet registered,
so the click has no effect and the dropdown never opens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel merged commit 43defa41c4 into main 2026-03-23 10:13:56 +01:00
marcel deleted branch feat/36-password-reset 2026-03-23 10:13:56 +01:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#49