feat: Phase 7 - Frontend scaffolding and auth (T-023, T-024, T-025)

T-023: Initialize React project
- Vite + React 19 + TypeScript scaffold
- Tailwind CSS v4 with @tailwindcss/vite plugin
- Dependencies: react-router-dom, axios, @tanstack/react-query, lucide-react
- Project structure: api/, components/, pages/, context/, types/, hooks/, lib/

T-024: API client and auth context
- Axios client with JWT interceptor (auto-attach token, clear on 401)
- login() and getMe() API functions
- AuthContext: user state, login, logout, isAuthenticated, isLoading
- Token persistence via localStorage with hydration on mount
- TypeScript types for all backend models

T-025: Login page and layout
- LoginPage with form, error handling, redirect on success
- Layout with sidebar + header + Outlet
- Sidebar with role-aware navigation (System only for admin)
- ProtectedRoute wrapper with role-based access control
- Routes: /login, /dashboard, /techniques, /tests, /system
This commit is contained in:
2026-02-06 16:09:50 +01:00
parent 52d230628d
commit 591b5df250
26 changed files with 3489 additions and 4 deletions

View File

@@ -0,0 +1,77 @@
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
import { login as apiLogin, getMe } from "../api/auth";
import type { User } from "../types/models";
/* ── Context shape ────────────────────────────────────────────────── */
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthState | undefined>(undefined);
/* ── Provider ─────────────────────────────────────────────────────── */
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// On mount — check for a persisted token and hydrate the user
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) {
setIsLoading(false);
return;
}
getMe()
.then(setUser)
.catch(() => localStorage.removeItem("token"))
.finally(() => setIsLoading(false));
}, []);
const login = useCallback(async (username: string, password: string) => {
const token = await apiLogin(username, password);
localStorage.setItem("token", token);
const me = await getMe();
setUser(me);
}, []);
const logout = useCallback(() => {
localStorage.removeItem("token");
setUser(null);
}, []);
return (
<AuthContext.Provider
value={{
user,
isAuthenticated: user !== null,
isLoading,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
);
}
/* ── Hook ─────────────────────────────────────────────────────────── */
export function useAuth(): AuthState {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within <AuthProvider>");
return ctx;
}