diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-04-27 07:10:41 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-04-27 07:10:41 +0900 |
| commit | e3576b4b3cb0428a6cc738289a66b7951a133320 (patch) | |
| tree | 2179760cf5ae1107c14254b109737899c706280d /src/client/atoms | |
| parent | 38b8fc0e9927c4146b4c8b309b2bcc644abd63d0 (diff) | |
| download | kioku-e3576b4b3cb0428a6cc738289a66b7951a133320.tar.gz kioku-e3576b4b3cb0428a6cc738289a66b7951a133320.tar.zst kioku-e3576b4b3cb0428a6cc738289a66b7951a133320.zip | |
fix(auth): redirect to login page on session expiry
Previously when the session expired, the API client cleared tokens but
the UI displayed "Invalid or expired token" instead of redirecting to
the login page. The root cause was that isAuthenticatedAtom was derived
from userAtom only as a re-evaluation trigger, while the actual value
came from apiClient.isAuthenticated(). On page reload userAtom is null,
so setting it to null on session expiry did not trigger a re-render and
ProtectedRoute never redirected.
Make userAtom (persisted via atomWithStorage) the single source of truth
for auth state, derive isAuthenticatedAtom from it, drop the redundant
apiClient.isAuthenticated(), and explicitly navigate to /login on
session expiry. Also trigger session expiry when a 401 comes back with
no refresh token available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/client/atoms')
| -rw-r--r-- | src/client/atoms/auth.ts | 27 |
1 files changed, 12 insertions, 15 deletions
diff --git a/src/client/atoms/auth.ts b/src/client/atoms/auth.ts index f618ccf..9ee69cf 100644 --- a/src/client/atoms/auth.ts +++ b/src/client/atoms/auth.ts @@ -1,17 +1,18 @@ import { atom, useSetAtom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; import { useEffect } from "react"; +import { useLocation } from "wouter"; import { apiClient, type User } from "../api/client"; -// Primitive atoms -export const userAtom = atom<User | null>(null); +// userAtom is the single source of truth for auth state. Persisted to +// localStorage so that the authenticated user survives page reloads alongside +// the tokens in apiClient's token storage. +export const userAtom = atomWithStorage<User | null>("kioku_user", null); export const authLoadingAtom = atom<boolean>(true); -// Derived atom - checks if user is authenticated via apiClient -export const isAuthenticatedAtom = atom<boolean>((get) => { - // We need to trigger re-evaluation when user changes - get(userAtom); - return apiClient.isAuthenticated(); -}); +export const isAuthenticatedAtom = atom<boolean>( + (get) => get(userAtom) !== null, +); // Action atom - login export const loginAtom = atom( @@ -36,22 +37,18 @@ export const logoutAtom = atom(null, (_get, set) => { export function useAuthInit() { const setAuthLoading = useSetAtom(authLoadingAtom); const setUser = useSetAtom(userAtom); + const [, navigate] = useLocation(); useEffect(() => { - // Check for existing auth on mount - const tokens = apiClient.getTokens(); - if (tokens) { - // We have tokens stored, but we don't have user info cached - // For now, just set authenticated state. User info will be fetched when needed. - } setAuthLoading(false); // Subscribe to session expired events from the API client const unsubscribe = apiClient.onSessionExpired(() => { apiClient.logout(); setUser(null); + navigate("/login", { replace: true }); }); return unsubscribe; - }, [setAuthLoading, setUser]); + }, [setAuthLoading, setUser, navigate]); } |
