generated from vincent/template-projet
Backend (FastAPI + SQLAlchemy): - Modèles : User, Client, Audit, Cible, Vulnérabilité, Action - Auth JWT (register/login/me) avec bcrypt - Routes CRUD complets : clients, audits, cibles, vulnérabilités, actions - Schémas Pydantic v2, migrations Alembic configurées - Rate limiting (slowapi), CORS, structure scanners/reports pour phase 2 Frontend (Next.js 14 App Router): - shadcn/ui : Button, Input, Card, Badge, Label - Page login avec gestion token JWT - Dashboard avec stats temps réel - Pages Clients (grille) et Audits (liste) avec recherche - Layout avec sidebar navigation + protection auth - Dockerfiles multi-stage (backend + frontend standalone) Infrastructure: - docker-compose.yml : postgres, redis, backend, frontend - docker-compose.prod.yml avec labels Traefik - .env.example complet - .gitignore mis à jour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
3.6 KiB
TypeScript
142 lines
3.6 KiB
TypeScript
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "";
|
|
|
|
type RequestOptions = {
|
|
method?: string;
|
|
body?: unknown;
|
|
token?: string;
|
|
};
|
|
|
|
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
const { method = "GET", body, token } = options;
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}/api${path}`, {
|
|
method,
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const error = await res.json().catch(() => ({ detail: "Erreur réseau" }));
|
|
throw new Error(error.detail ?? "Erreur inconnue");
|
|
}
|
|
|
|
if (res.status === 204) return undefined as T;
|
|
return res.json();
|
|
}
|
|
|
|
// Auth
|
|
export const authApi = {
|
|
login: (email: string, password: string) => {
|
|
const form = new URLSearchParams({ username: email, password });
|
|
return fetch(`${API_BASE}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: form.toString(),
|
|
}).then((r) => {
|
|
if (!r.ok) throw new Error("Identifiants incorrects");
|
|
return r.json() as Promise<{ access_token: string; token_type: string }>;
|
|
});
|
|
},
|
|
me: (token: string) => request<User>("/auth/me", { token }),
|
|
};
|
|
|
|
// Clients
|
|
export const clientsApi = {
|
|
list: (token: string) => request<Client[]>("/clients/", { token }),
|
|
get: (id: number, token: string) => request<Client>(`/clients/${id}`, { token }),
|
|
create: (data: ClientCreate, token: string) =>
|
|
request<Client>("/clients/", { method: "POST", body: data, token }),
|
|
update: (id: number, data: Partial<ClientCreate>, token: string) =>
|
|
request<Client>(`/clients/${id}`, { method: "PATCH", body: data, token }),
|
|
delete: (id: number, token: string) =>
|
|
request<void>(`/clients/${id}`, { method: "DELETE", token }),
|
|
};
|
|
|
|
// Audits
|
|
export const auditsApi = {
|
|
list: (token: string, clientId?: number) =>
|
|
request<Audit[]>(`/audits/${clientId ? `?client_id=${clientId}` : ""}`, { token }),
|
|
get: (id: number, token: string) => request<AuditDetail>(`/audits/${id}`, { token }),
|
|
create: (data: AuditCreate, token: string) =>
|
|
request<Audit>("/audits/", { method: "POST", body: data, token }),
|
|
update: (id: number, data: Partial<AuditCreate>, token: string) =>
|
|
request<Audit>(`/audits/${id}`, { method: "PATCH", body: data, token }),
|
|
};
|
|
|
|
// Types
|
|
export type User = {
|
|
id: number;
|
|
email: string;
|
|
fullName: string;
|
|
isActive: boolean;
|
|
isAdmin: boolean;
|
|
};
|
|
|
|
export type Client = {
|
|
id: number;
|
|
nom: string;
|
|
contact: string | null;
|
|
email: string | null;
|
|
telephone: string | null;
|
|
notes: string | null;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type ClientCreate = {
|
|
nom: string;
|
|
contact?: string;
|
|
email?: string;
|
|
telephone?: string;
|
|
notes?: string;
|
|
};
|
|
|
|
export type Audit = {
|
|
id: number;
|
|
clientId: number;
|
|
nom: string;
|
|
statut: "planifie" | "en_cours" | "termine" | "annule";
|
|
dateDebut: string | null;
|
|
dateFin: string | null;
|
|
scoreGlobal: number | null;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type AuditCreate = {
|
|
clientId: number;
|
|
nom: string;
|
|
dateDebut?: string;
|
|
};
|
|
|
|
export type AuditDetail = Audit & {
|
|
cibles: Cible[];
|
|
vulnerabilites: Vulnerabilite[];
|
|
};
|
|
|
|
export type Cible = {
|
|
id: number;
|
|
auditId: number;
|
|
type: "ip" | "domaine" | "subnet";
|
|
valeur: string;
|
|
validee: boolean;
|
|
};
|
|
|
|
export type Vulnerabilite = {
|
|
id: number;
|
|
auditId: number;
|
|
criticite: "critique" | "important" | "modere" | "faible";
|
|
titre: string;
|
|
description: string;
|
|
recommandation: string;
|
|
cve: string | null;
|
|
cvssScore: number | null;
|
|
cible: string | null;
|
|
};
|