frontend: add session API, credentialed fetch, 401-triggered relogin
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
||||
ProgressionMetric,
|
||||
ProgressionPoint,
|
||||
ReviewQueuePage,
|
||||
SessionInfo,
|
||||
SyncRun,
|
||||
SyncStatus,
|
||||
WorkoutKind,
|
||||
@@ -17,8 +18,17 @@ const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080";
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${path}`, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
...init,
|
||||
});
|
||||
if (res.status === 401 && path !== "/api/session/me") {
|
||||
// An established session expired mid-use (not the initial "am I logged
|
||||
// in" check, which LoginGate itself interprets) -- reload so LoginGate
|
||||
// re-checks and shows the login screen instead of leaving the SPA in a
|
||||
// half-authenticated state.
|
||||
window.location.href = "/";
|
||||
throw new Error(`${init?.method ?? "GET"} ${path} failed: 401 (session expired)`);
|
||||
}
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`${init?.method ?? "GET"} ${path} failed: ${res.status} ${body}`);
|
||||
@@ -28,6 +38,12 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// Session (app login via OIDC -- distinct from the Garmin credential
|
||||
// login below). No logout()/login() methods: those are plain <a href>
|
||||
// full-page navigations (see LoginGate.tsx), not fetches, since the OIDC
|
||||
// flow and Keycloak's own logout redirect need real browser navigation.
|
||||
getSessionInfo: () => request<SessionInfo>("/api/session/me"),
|
||||
|
||||
// Auth
|
||||
login: () => request<AuthResponse>("/api/auth/login", { method: "POST" }),
|
||||
submitMFA: (code: string) =>
|
||||
|
||||
Reference in New Issue
Block a user