M1: implement operational core

Demo auth, seed import/reset, dashboard, vehicle/booking list+detail, audit trail. Backend: 19 tests passing, ruff clean. Frontend: React Router shell, typed API client, responsive pages. Verified end-to-end via curl and browser.
This commit is contained in:
NuklearRabbit
2026-08-01 21:20:53 +02:00
parent 04d26f1f2e
commit 03c5b60235
47 changed files with 2518 additions and 70 deletions
+54
View File
@@ -0,0 +1,54 @@
import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
import { api, ApiError } from "../api/client";
import type { CurrentUser, Role } from "../api/types";
interface AuthState {
user: CurrentUser | null;
loading: boolean;
loginAs: (role: Role) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthState | undefined>(undefined);
const STORAGE_KEY = "mobilityops.demo-user";
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<CurrentUser | null>(() => {
const stored = sessionStorage.getItem(STORAGE_KEY);
return stored ? (JSON.parse(stored) as CurrentUser) : null;
});
const [loading, setLoading] = useState(false);
const loginAs = useCallback(async (role: Role) => {
setLoading(true);
try {
const loggedIn = await api.post<CurrentUser>("/api/v1/demo/login", { role });
setUser(loggedIn);
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(loggedIn));
} finally {
setLoading(false);
}
}, []);
const logout = useCallback(() => {
setUser(null);
sessionStorage.removeItem(STORAGE_KEY);
}, []);
return (
<AuthContext.Provider value={{ user, loading, loginAs, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth(): AuthState {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
export function isSessionExpired(error: unknown): boolean {
return error instanceof ApiError && error.status === 401;
}