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
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:
258
docs/reset-password-reference.md
Normal file
258
docs/reset-password-reference.md
Normal 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
|
||||
Reference in New Issue
Block a user