diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/App.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/api/client.ts | 13 | ||||
| -rw-r--r-- | src/client/components/ProtectedRoute.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/pages/DeckDetailPage.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/pages/HomePage.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/pages/LoginPage.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/pages/NoteTypesPage.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/pages/StudyPage.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/stores/auth.test.tsx | 1 | ||||
| -rw-r--r-- | src/client/stores/auth.tsx | 24 |
10 files changed, 37 insertions, 8 deletions
diff --git a/src/client/App.test.tsx b/src/client/App.test.tsx index fe870b7..2617f44 100644 --- a/src/client/App.test.tsx +++ b/src/client/App.test.tsx @@ -17,6 +17,7 @@ vi.mock("./api/client", () => ({ isAuthenticated: vi.fn(), getTokens: vi.fn(), getAuthHeader: vi.fn(), + onSessionExpired: vi.fn(() => vi.fn()), rpc: { api: { decks: { diff --git a/src/client/api/client.ts b/src/client/api/client.ts index b39f064..c91160d 100644 --- a/src/client/api/client.ts +++ b/src/client/api/client.ts @@ -58,10 +58,13 @@ export function createClient(baseUrl: string): Client { return hc<AppType>(baseUrl); } +export type SessionExpiredCallback = () => void; + export class ApiClient { private tokenStorage: TokenStorage; private refreshPromise: Promise<boolean> | null = null; private baseUrl: string; + private sessionExpiredCallback: SessionExpiredCallback | null = null; public readonly rpc: Client; constructor(options: ApiClientOptions = {}) { @@ -70,6 +73,13 @@ export class ApiClient { this.rpc = this.createAuthenticatedClient(); } + onSessionExpired(callback: SessionExpiredCallback): () => void { + this.sessionExpiredCallback = callback; + return () => { + this.sessionExpiredCallback = null; + }; + } + private createAuthenticatedClient(): Client { return hc<AppType>(this.baseUrl, { fetch: async (input: RequestInfo | URL, init?: RequestInit) => { @@ -156,8 +166,9 @@ export class ApiClient { }); if (!res.ok) { - // Clear tokens if refresh fails + // Clear tokens if refresh fails and notify listeners this.tokenStorage.clearTokens(); + this.sessionExpiredCallback?.(); return false; } diff --git a/src/client/components/ProtectedRoute.test.tsx b/src/client/components/ProtectedRoute.test.tsx index 85a12cd..25e73a3 100644 --- a/src/client/components/ProtectedRoute.test.tsx +++ b/src/client/components/ProtectedRoute.test.tsx @@ -15,6 +15,7 @@ vi.mock("../api/client", () => ({ logout: vi.fn(), isAuthenticated: vi.fn(), getTokens: vi.fn(), + onSessionExpired: vi.fn(() => vi.fn()), }, ApiClientError: class ApiClientError extends Error { constructor( diff --git a/src/client/pages/DeckDetailPage.test.tsx b/src/client/pages/DeckDetailPage.test.tsx index e02302f..1ef6ae7 100644 --- a/src/client/pages/DeckDetailPage.test.tsx +++ b/src/client/pages/DeckDetailPage.test.tsx @@ -17,6 +17,7 @@ vi.mock("../api/client", () => ({ isAuthenticated: vi.fn(), getTokens: vi.fn(), getAuthHeader: vi.fn(), + onSessionExpired: vi.fn(() => vi.fn()), rpc: { api: { decks: { diff --git a/src/client/pages/HomePage.test.tsx b/src/client/pages/HomePage.test.tsx index 5b8489a..944dd31 100644 --- a/src/client/pages/HomePage.test.tsx +++ b/src/client/pages/HomePage.test.tsx @@ -18,6 +18,7 @@ vi.mock("../api/client", () => ({ isAuthenticated: vi.fn(), getTokens: vi.fn(), getAuthHeader: vi.fn(), + onSessionExpired: vi.fn(() => vi.fn()), rpc: { api: { decks: { diff --git a/src/client/pages/LoginPage.test.tsx b/src/client/pages/LoginPage.test.tsx index e4dac95..a3efa8d 100644 --- a/src/client/pages/LoginPage.test.tsx +++ b/src/client/pages/LoginPage.test.tsx @@ -16,6 +16,7 @@ vi.mock("../api/client", () => ({ logout: vi.fn(), isAuthenticated: vi.fn(), getTokens: vi.fn(), + onSessionExpired: vi.fn(() => vi.fn()), }, ApiClientError: class ApiClientError extends Error { constructor( diff --git a/src/client/pages/NoteTypesPage.test.tsx b/src/client/pages/NoteTypesPage.test.tsx index c2df7f5..8364d17 100644 --- a/src/client/pages/NoteTypesPage.test.tsx +++ b/src/client/pages/NoteTypesPage.test.tsx @@ -18,6 +18,7 @@ vi.mock("../api/client", () => ({ isAuthenticated: vi.fn(), getTokens: vi.fn(), getAuthHeader: vi.fn(), + onSessionExpired: vi.fn(() => vi.fn()), }, ApiClientError: class ApiClientError extends Error { constructor( diff --git a/src/client/pages/StudyPage.test.tsx b/src/client/pages/StudyPage.test.tsx index 146322a..bc87b9d 100644 --- a/src/client/pages/StudyPage.test.tsx +++ b/src/client/pages/StudyPage.test.tsx @@ -17,6 +17,7 @@ vi.mock("../api/client", () => ({ isAuthenticated: vi.fn(), getTokens: vi.fn(), getAuthHeader: vi.fn(), + onSessionExpired: vi.fn(() => vi.fn()), rpc: { api: { decks: { diff --git a/src/client/stores/auth.test.tsx b/src/client/stores/auth.test.tsx index 72ab9e3..1769011 100644 --- a/src/client/stores/auth.test.tsx +++ b/src/client/stores/auth.test.tsx @@ -14,6 +14,7 @@ vi.mock("../api/client", () => ({ logout: vi.fn(), isAuthenticated: vi.fn(), getTokens: vi.fn(), + onSessionExpired: vi.fn(() => vi.fn()), }, ApiClientError: class ApiClientError extends Error { constructor( diff --git a/src/client/stores/auth.tsx b/src/client/stores/auth.tsx index b34717b..3f2681b 100644 --- a/src/client/stores/auth.tsx +++ b/src/client/stores/auth.tsx @@ -31,6 +31,15 @@ export interface AuthProviderProps { export function AuthProvider({ children }: AuthProviderProps) { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState( + apiClient.isAuthenticated(), + ); + + const logout = useCallback(() => { + apiClient.logout(); + setUser(null); + setIsAuthenticated(false); + }, []); // Check for existing auth on mount useEffect(() => { @@ -45,18 +54,19 @@ export function AuthProvider({ children }: AuthProviderProps) { } }, []); + // Subscribe to session expired events from the API client + useEffect(() => { + return apiClient.onSessionExpired(() => { + logout(); + }); + }, [logout]); + const login = useCallback(async (username: string, password: string) => { const response = await apiClient.login(username, password); setUser(response.user); + setIsAuthenticated(true); }, []); - const logout = useCallback(() => { - apiClient.logout(); - setUser(null); - }, []); - - const isAuthenticated = apiClient.isAuthenticated(); - const value = useMemo<AuthContextValue>( () => ({ user, |
