- 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.
6.0 KiB
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/mewith stale tokens - multi-tab synchronization in the frontend
High-Level Behavior
After a successful password reset:
- Backend updates the password hash.
- Backend increments the user's
token_version. - Backend clears the
tokenauth cookie in the reset response. - Existing JWTs in other tabs/devices become invalid because their embedded
token_versionno longer matches. - 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 to0for backward compatibility.to_dict()persiststoken_version.
2) JWT issuance includes token version
File: backend/api/auth_api.py (/auth/login)
JWT payload now includes:
emailuser_idtoken_versionexp
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) withuser.token_version - if mismatch, return:
- status:
401 - code:
INVALID_TOKEN
- status:
4) reset-password invalidates sessions
File: backend/api/auth_api.py (/auth/reset-password)
On success:
- hash and store new password
- clear
reset_tokenandreset_token_created - increment
user.token_version - persist user
- clear
tokencookie 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_versionvs DBtoken_version - return
Noneif 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
storageevents - if logout event arrives, applies logged-out state and redirects to
/auth/loginwhen outside/auth/*
- listens to
checkAuth()now funnels failed/api/auth/mechecks throughlogoutUser()
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.tsfrontend/vue-app/src/main.ts
Implemented:
installUnauthorizedFetchInterceptor()wraps globalfetch- if any response is
401, frontend:- calls
logoutUser() - redirects to
/auth(unless already on/auth/*)
- calls
This ensures protected pages consistently return users to auth landing when a session is invalid.
Sequence Diagram (Reset Success)
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)
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_TOKEN400 INVALID_TOKEN400 TOKEN_TIMESTAMP_MISSING400 TOKEN_EXPIRED
Reset password
POST /api/auth/reset-password
Request body:
{
"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
storageevents.
Test Coverage
Backend:
backend/tests/test_auth_api.py- includes regression test ensuring old JWT fails
/auth/meafter 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
401interceptor logout and redirect behavior.
Troubleshooting Checklist
- If stale sessions still appear valid:
- verify
token_versionexists in user records - confirm
/auth/loginincludestoken_versionclaim - confirm
/auth/mecompares JWT vs DB token_version - confirm
/auth/reset-passwordincrements token_version
- verify
- If other tabs do not redirect:
- verify
initAuthSync()is called inmain.ts - verify
logoutUser()is called on reset success - check browser supports storage events across tabs for same origin
- verify