TASK-004: Bootstrap frontend auth state from backend session check

Validates session on app mount by calling GET /api/auth/session instead of relying
solely on cached sessionStorage. This ensures the UI state always reflects server
reality — expired or revoked sessions are detected immediately.

Changes:
- Backend: Add GET /api/auth/session endpoint (requires valid session, returns 200/401)
- Frontend: Add useSessionValidation hook for mount-time validation
- Frontend: Add SessionValidationLoading component for validation spinner
- Frontend: Update AuthProvider to call validation on mount with loading state
- Frontend: Add validateSession API function
- Docs: Update Features.md with session validation behavior
- Docs: Update Web-Development.md with session validation pattern

Handles three outcomes:
1. Valid session (200): Proceed with cached state
2. Invalid session (401): Clear sessionStorage and redirect to login
3. Network error: Don't logout (backend may be temporarily unreachable)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-26 12:00:21 +02:00
parent d982fe3efc
commit 29daaa9906
11 changed files with 1314 additions and 15 deletions

View File

@@ -202,6 +202,68 @@ class TestRequireAuth:
assert call_count == 2
# ---------------------------------------------------------------------------
# Session validation (Task 4)
# ---------------------------------------------------------------------------
class TestValidateSession:
"""GET /api/auth/session."""
async def test_validate_session_returns_200_with_valid_token(
self, client: AsyncClient
) -> None:
"""Validate session returns 200 for a valid authenticated request."""
await _do_setup(client)
await _login(client)
response = await client.get("/api/auth/session")
assert response.status_code == 200
assert response.json() == {"valid": True}
async def test_validate_session_returns_401_without_token(
self, client: AsyncClient
) -> None:
"""Validate session returns 401 when no token is present."""
await _do_setup(client)
response = await client.get("/api/auth/session")
assert response.status_code == 401
async def test_validate_session_returns_401_with_invalid_token(
self, client: AsyncClient
) -> None:
"""Validate session returns 401 for an invalid or expired token."""
await _do_setup(client)
response = await client.get(
"/api/auth/session",
headers={"Authorization": "Bearer invalidtoken"},
)
assert response.status_code == 401
async def test_validate_session_with_cookie(
self, client: AsyncClient
) -> None:
"""Validate session works with cookie-based authentication."""
await _do_setup(client)
token = await _login(client)
# Login sets the cookie on the client automatically via httpx.
response = await client.get("/api/auth/session")
assert response.status_code == 200
assert response.json() == {"valid": True}
async def test_validate_session_after_logout(
self, client: AsyncClient
) -> None:
"""Validate session returns 401 after logout."""
await _do_setup(client)
token = await _login(client)
await client.post("/api/auth/logout")
response = await client.get(
"/api/auth/session",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 401
# ---------------------------------------------------------------------------
# Session-token cache (Task 4)
# ---------------------------------------------------------------------------