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

29
frontend/src/api/auth.ts Normal file
View File

@@ -0,0 +1,29 @@
import client from "./client";
import type { User } from "../types/models";
interface TokenResponse {
access_token: string;
token_type: string;
}
/** Authenticate and return the access token. */
export async function login(
username: string,
password: string,
): Promise<string> {
const params = new URLSearchParams();
params.append("username", username);
params.append("password", password);
const { data } = await client.post<TokenResponse>("/auth/login", params, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
return data.access_token;
}
/** Fetch the currently authenticated user profile. */
export async function getMe(): Promise<User> {
const { data } = await client.get<User>("/auth/me");
return data;
}

View File

@@ -0,0 +1,28 @@
import axios from "axios";
const client = axios.create({
baseURL: "http://localhost:8000/api/v1",
headers: { "Content-Type": "application/json" },
});
// Attach the JWT token on every request (if present)
client.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// On 401, clear token so the UI can redirect to login
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem("token");
}
return Promise.reject(error);
},
);
export default client;