generated from vincent/template-projet
feat: Phase 1 — socle backend FastAPI + frontend Next.js
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>
This commit is contained in:
141
frontend/lib/api.ts
Normal file
141
frontend/lib/api.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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;
|
||||
};
|
||||
20
frontend/lib/auth.ts
Normal file
20
frontend/lib/auth.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
const TOKEN_KEY = "auditshield_token";
|
||||
|
||||
export function getToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function removeToken(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
return !!getToken();
|
||||
}
|
||||
6
frontend/lib/utils.ts
Normal file
6
frontend/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user