Feature: Password reset flow via Gmail OAuth (bypass Keycloak SMTP) #132

Closed
opened 2026-03-21 16:10:55 +00:00 by forgejo_admin · 0 comments

Type

Feature

Lineage

plan-wkq → Phase 11 (Girls Tryout — March 24)
Supersedes basketball-api #131 (Keycloak SMTP not configured)

Repo

forgejo_admin/basketball-api (backend) + forgejo_admin/westside-app (frontend)

User Story

As a player/parent
I want to reset my password via email
So that I can regain access to my account without contacting an admin

Context

Keycloak's built-in "Forgot Password?" flow requires SMTP, which it doesn't support via OAuth. Our platform uses Gmail OAuth exclusively (westsidebasketball@gmail.com) — no app passwords, no SendGrid. Rather than fight Keycloak's SMTP limitation, we bypass it entirely: basketball-api generates the reset token via Keycloak admin API and sends the email through the existing Gmail OAuth pipeline. Single email path, single auth mechanism.

The existing Gmail OAuth tokens are in k8s secret gmail-oauth-westsidebasketball in the basketball-api namespace. The email service (services/email.py) already sends registration confirmations, roster exports, and tryout announcements through this pipeline.

File Targets

basketball-api:

  • src/basketball_api/routes/password_reset.py — new router with two endpoints
  • src/basketball_api/services/email.py — add send_password_reset_email() function
  • src/basketball_api/main.py — register the new router
  • tests/test_password_reset.py — new test file

westside-app:

  • src/routes/forgot-password/+page.svelte — new page, email input form
  • src/routes/reset-password/+page.svelte — new page, new password form (reads token from query param)
  • src/routes/+layout.svelte — add /forgot-password and /reset-password to PUBLIC_ROUTES

Files NOT to touch:

  • Keycloak theme files — CSS changes are separate work
  • Existing registration flow — this is additive

Acceptance Criteria

  • POST /api/password-reset/request accepts {email}, generates a time-limited reset token, stores it, sends reset link via Gmail OAuth to the player's email
  • POST /api/password-reset/confirm accepts {token, new_password}, validates token, calls Keycloak admin API to set the new password
  • /forgot-password page: enter email → "check your email" confirmation
  • /reset-password?token=X page: enter new password → success → redirect to signin
  • Reset tokens expire after 1 hour
  • Invalid/expired tokens show a clear error message
  • Email comes from westsidebasketball@gmail.com with "Westside Kings & Queens" display name
  • Keycloak "Forgot Password?" link hidden or redirected in theme (separate PR OK)

Test Expectations

  • Unit test: request endpoint returns 200 for valid email, 200 for unknown email (no user enumeration)
  • Unit test: confirm endpoint rejects expired/invalid tokens
  • Unit test: confirm endpoint calls Keycloak admin API to set password
  • Integration test: full flow with test Keycloak user
  • Run command: pytest tests/test_password_reset.py

Constraints

  • Use existing Gmail OAuth pipeline in services/email.py — no new auth mechanisms
  • Reset tokens stored in database (new table) or signed JWTs — architect's choice
  • No user enumeration: both valid and invalid emails return the same response
  • Must work on mobile — parents will click the reset link on their phone
  • Match existing westside-app dark theme / design system

Checklist

  • PR opened (may be split: basketball-api PR + westside-app PR)
  • Tests pass
  • No unrelated changes
  • basketball-api #131 — the bug this replaces (Keycloak SMTP not configured)
  • feedback_gmail_oauth_not_smtp.md — all email = Gmail OAuth, no exceptions
  • westside-app #57 — spike that discovered the SMTP gap
  • k8s secret gmail-oauth-westsidebasketball — existing OAuth tokens
  • src/basketball_api/services/email.py — existing email service to extend
### Type Feature ### Lineage `plan-wkq` → Phase 11 (Girls Tryout — March 24) Supersedes basketball-api #131 (Keycloak SMTP not configured) ### Repo `forgejo_admin/basketball-api` (backend) + `forgejo_admin/westside-app` (frontend) ### User Story As a player/parent I want to reset my password via email So that I can regain access to my account without contacting an admin ### Context Keycloak's built-in "Forgot Password?" flow requires SMTP, which it doesn't support via OAuth. Our platform uses Gmail OAuth exclusively (westsidebasketball@gmail.com) — no app passwords, no SendGrid. Rather than fight Keycloak's SMTP limitation, we bypass it entirely: basketball-api generates the reset token via Keycloak admin API and sends the email through the existing Gmail OAuth pipeline. Single email path, single auth mechanism. The existing Gmail OAuth tokens are in k8s secret `gmail-oauth-westsidebasketball` in the `basketball-api` namespace. The email service (`services/email.py`) already sends registration confirmations, roster exports, and tryout announcements through this pipeline. ### File Targets **basketball-api:** - `src/basketball_api/routes/password_reset.py` — new router with two endpoints - `src/basketball_api/services/email.py` — add `send_password_reset_email()` function - `src/basketball_api/main.py` — register the new router - `tests/test_password_reset.py` — new test file **westside-app:** - `src/routes/forgot-password/+page.svelte` — new page, email input form - `src/routes/reset-password/+page.svelte` — new page, new password form (reads token from query param) - `src/routes/+layout.svelte` — add `/forgot-password` and `/reset-password` to PUBLIC_ROUTES Files NOT to touch: - Keycloak theme files — CSS changes are separate work - Existing registration flow — this is additive ### Acceptance Criteria - [ ] `POST /api/password-reset/request` accepts `{email}`, generates a time-limited reset token, stores it, sends reset link via Gmail OAuth to the player's email - [ ] `POST /api/password-reset/confirm` accepts `{token, new_password}`, validates token, calls Keycloak admin API to set the new password - [ ] `/forgot-password` page: enter email → "check your email" confirmation - [ ] `/reset-password?token=X` page: enter new password → success → redirect to signin - [ ] Reset tokens expire after 1 hour - [ ] Invalid/expired tokens show a clear error message - [ ] Email comes from `westsidebasketball@gmail.com` with "Westside Kings & Queens" display name - [ ] Keycloak "Forgot Password?" link hidden or redirected in theme (separate PR OK) ### Test Expectations - [ ] Unit test: request endpoint returns 200 for valid email, 200 for unknown email (no user enumeration) - [ ] Unit test: confirm endpoint rejects expired/invalid tokens - [ ] Unit test: confirm endpoint calls Keycloak admin API to set password - [ ] Integration test: full flow with test Keycloak user - Run command: `pytest tests/test_password_reset.py` ### Constraints - Use existing Gmail OAuth pipeline in `services/email.py` — no new auth mechanisms - Reset tokens stored in database (new table) or signed JWTs — architect's choice - No user enumeration: both valid and invalid emails return the same response - Must work on mobile — parents will click the reset link on their phone - Match existing westside-app dark theme / design system ### Checklist - [ ] PR opened (may be split: basketball-api PR + westside-app PR) - [ ] Tests pass - [ ] No unrelated changes ### Related - basketball-api #131 — the bug this replaces (Keycloak SMTP not configured) - `feedback_gmail_oauth_not_smtp.md` — all email = Gmail OAuth, no exceptions - westside-app #57 — spike that discovered the SMTP gap - k8s secret `gmail-oauth-westsidebasketball` — existing OAuth tokens - `src/basketball_api/services/email.py` — existing email service to extend
forgejo_admin 2026-03-21 16:43:52 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/basketball-api#132
No description provided.