frontend: add session API, credentialed fetch, 401-triggered relogin
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
|||||||
ProgressionMetric,
|
ProgressionMetric,
|
||||||
ProgressionPoint,
|
ProgressionPoint,
|
||||||
ReviewQueuePage,
|
ReviewQueuePage,
|
||||||
|
SessionInfo,
|
||||||
SyncRun,
|
SyncRun,
|
||||||
SyncStatus,
|
SyncStatus,
|
||||||
WorkoutKind,
|
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> {
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const res = await fetch(`${BASE_URL}${path}`, {
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
...init,
|
...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) {
|
if (!res.ok) {
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
throw new Error(`${init?.method ?? "GET"} ${path} failed: ${res.status} ${body}`);
|
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 = {
|
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
|
// Auth
|
||||||
login: () => request<AuthResponse>("/api/auth/login", { method: "POST" }),
|
login: () => request<AuthResponse>("/api/auth/login", { method: "POST" }),
|
||||||
submitMFA: (code: string) =>
|
submitMFA: (code: string) =>
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ export interface AuthResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionInfo {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DetailFillProgress {
|
export interface DetailFillProgress {
|
||||||
Done: number;
|
Done: number;
|
||||||
Total: number;
|
Total: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user