feat: enhance child edit and view components with improved form handling and validation
All checks were successful
Chore App Build and Push Docker Images / build-and-push (push) Successful in 1m4s

- Added `requireDirty` prop to `EntityEditForm` for dirty state management.
- Updated `ChildEditView` to handle initial data loading and image selection more robustly.
- Refactored `ChildView` to remove unused reward dialog logic and prevent API calls in child mode.
- Improved type definitions for form fields and initial data in `ChildEditView`.
- Enhanced error handling in form submissions across components.
- Implemented cross-tab logout synchronization on password reset in the auth store.
- Added tests for login and entity edit form functionalities to ensure proper behavior.
- Introduced global fetch interceptor for handling unauthorized responses.
- Documented password reset flow and its implications on session management.
This commit is contained in:
2026-02-17 17:18:03 -05:00
parent 5e22e5e0ee
commit 31ea76f013
29 changed files with 1000 additions and 164 deletions

View File

@@ -0,0 +1,258 @@
# Password Reset Reference
This document explains the full password reset and forced re-auth flow implemented in the project.
## Scope
This covers:
- reset token validation and reset submission
- JWT invalidation after reset
- behavior of `/auth/me` with stale tokens
- multi-tab synchronization in the frontend
---
## High-Level Behavior
After a successful password reset:
1. Backend updates the password hash.
2. Backend increments the user's `token_version`.
3. Backend clears the `token` auth cookie in the reset response.
4. Existing JWTs in other tabs/devices become invalid because their embedded `token_version` no longer matches.
5. Frontend broadcasts a logout sync event so other tabs immediately redirect to login.
---
## Backend Components
### 1) User model versioning
File: `backend/models/user.py`
- Added `token_version: int = 0`.
- `from_dict()` defaults missing value to `0` for backward compatibility.
- `to_dict()` persists `token_version`.
### 2) JWT issuance includes token version
File: `backend/api/auth_api.py` (`/auth/login`)
JWT payload now includes:
- `email`
- `user_id`
- `token_version`
- `exp`
### 3) `/auth/me` rejects stale tokens
File: `backend/api/auth_api.py` (`/auth/me`)
Flow:
- decode JWT
- load user from DB
- compare `payload.token_version` (default 0) with `user.token_version`
- if mismatch, return:
- status: `401`
- code: `INVALID_TOKEN`
### 4) reset-password invalidates sessions
File: `backend/api/auth_api.py` (`/auth/reset-password`)
On success:
- hash and store new password
- clear `reset_token` and `reset_token_created`
- increment `user.token_version`
- persist user
- clear `token` cookie in response (`expires=0`, `httponly=True`, `secure=True`, `samesite='Strict'`)
### 5) shared auth utility enforcement
File: `backend/api/utils.py` (`get_current_user_id`)
Protected endpoints that use this helper also enforce token version:
- decode JWT
- load user by `user_id`
- compare JWT `token_version` vs DB `token_version`
- return `None` if mismatch
---
## Frontend Components
### 1) Reset password page
File: `frontend/vue-app/src/components/auth/ResetPassword.vue`
On successful `/api/auth/reset-password`:
- calls `logoutUser()` from auth store
- still shows success modal
- Sign In action navigates to login
### 2) Cross-tab logout sync
File: `frontend/vue-app/src/stores/auth.ts`
Implemented:
- logout broadcast key: `authSyncEvent`
- `logoutUser()`:
- applies local logged-out state
- writes logout event to localStorage
- `initAuthSync()`:
- listens to `storage` events
- if logout event arrives, applies logged-out state and redirects to `/auth/login` when outside `/auth/*`
- `checkAuth()` now funnels failed `/api/auth/me` checks through `logoutUser()`
### 3) Sync bootstrap
File: `frontend/vue-app/src/main.ts`
- calls `initAuthSync()` at app startup.
### 4) Global `401 Unauthorized` handling
Files:
- `frontend/vue-app/src/common/api.ts`
- `frontend/vue-app/src/main.ts`
Implemented:
- `installUnauthorizedFetchInterceptor()` wraps global `fetch`
- if any response is `401`, frontend:
- calls `logoutUser()`
- redirects to `/auth` (unless already on `/auth/*`)
This ensures protected pages consistently return users to auth landing when a session is invalid.
---
## Sequence Diagram (Reset Success)
```mermaid
sequenceDiagram
participant U as User (Tab A)
participant FE as ResetPassword.vue
participant BE as auth_api.py
participant DB as users_db
participant LS as localStorage
participant T2 as Browser Tab B
U->>FE: Submit new password + token
FE->>BE: POST /api/auth/reset-password
BE->>DB: Validate reset token + expiry
BE->>DB: Update password hash
BE->>DB: token_version = token_version + 1
BE-->>FE: 200 + clear auth cookie
FE->>LS: logoutUser() writes authSyncEvent
LS-->>T2: storage event(authSyncEvent: logout)
T2->>T2: clear auth state
T2->>T2: redirect /auth/login
```
---
## Sequence Diagram (Stale Token Check)
```mermaid
sequenceDiagram
participant T as Any Tab with old JWT
participant BE as /auth/me
participant DB as users_db
T->>BE: GET /auth/me (old JWT token_version=N)
BE->>DB: Load user (current token_version=N+1)
BE-->>T: 401 { code: INVALID_TOKEN }
```
---
## Example API Calls
### Validate reset token
`GET /api/auth/validate-reset-token?token=<token>`
Possible failures:
- `400 MISSING_TOKEN`
- `400 INVALID_TOKEN`
- `400 TOKEN_TIMESTAMP_MISSING`
- `400 TOKEN_EXPIRED`
### Reset password
`POST /api/auth/reset-password`
Request body:
```json
{
"token": "<reset-token>",
"password": "newStrongPassword123"
}
```
Success:
- `200 { "message": "Password has been reset" }`
- response also clears auth cookie
### Auth check after reset with stale JWT
`GET /api/auth/me`
Expected:
- `401 { "error": "Invalid token", "code": "INVALID_TOKEN" }`
---
## SSE vs Cross-Tab Sync
Current design intentionally does **not** rely on SSE to enforce logout correctness.
Why:
- Security correctness is guaranteed by cookie clearing + token_version invalidation.
- SSE can improve UX but is not required for correctness.
- Cross-tab immediate UX is handled client-side via localStorage `storage` events.
---
## Test Coverage
Backend:
- `backend/tests/test_auth_api.py`
- includes regression test ensuring old JWT fails `/auth/me` after reset.
Frontend:
- `frontend/vue-app/src/stores/__tests__/auth.childmode.spec.ts`
- includes cross-tab storage logout behavior.
- `frontend/vue-app/src/common/__tests__/api.interceptor.spec.ts`
- verifies global `401` interceptor logout and redirect behavior.
---
## Troubleshooting Checklist
- If stale sessions still appear valid:
- verify `token_version` exists in user records
- confirm `/auth/login` includes `token_version` claim
- confirm `/auth/me` compares JWT vs DB token_version
- confirm `/auth/reset-password` increments token_version
- If other tabs do not redirect:
- verify `initAuthSync()` is called in `main.ts`
- verify `logoutUser()` is called on reset success
- check browser supports storage events across tabs for same origin