@@ -73,6 +86,21 @@ export default function Login() {
/>
+
+
+ setReponseCaptcha(e.target.value)}
+ placeholder="Votre réponse"
+ autoComplete="off"
+ />
+
+
{erreur &&
{erreur}
}
-
- ou
-
-
-
-
diff --git a/DataSentinel/src/pages/Users/Users.jsx b/DataSentinel/src/pages/Users/Users.jsx
index 345e29b..b0e5b79 100644
--- a/DataSentinel/src/pages/Users/Users.jsx
+++ b/DataSentinel/src/pages/Users/Users.jsx
@@ -1,76 +1,273 @@
// src/pages/Users/Users.jsx
-// Page Utilisateurs : gestion des utilisateurs (simulation)
-import { useState } from 'react';
-import { Plus, Edit, Trash2 } from 'lucide-react';
+// Page Utilisateurs : gestion réelle des utilisateurs via l'API admin
+import { useState, useEffect, useCallback } from 'react';
+import { Plus, Edit, Trash2, KeyRound } from 'lucide-react';
import DataTable from '../../components/common/DataTable';
import Button from '../../components/common/Button';
+import LoadingSpinner from '../../components/common/LoadingSpinner';
+import ErrorMessage from '../../components/common/ErrorMessage';
+import {
+ getUsers,
+ createUser,
+ updateUser,
+ deleteUser,
+ resetUserPassword,
+} from '../../services/api';
import styles from './Users.module.css';
-/* Données simulées pour les utilisateurs */
-const mockUsers = [
- { id: 1, nom: 'Alice Dupont', email: 'alice@xefi.fr', role: 'Admin', actif: true },
- { id: 2, nom: 'Bob Martin', email: 'bob@xefi.fr', role: 'Utilisateur', actif: true },
- { id: 3, nom: 'Charlie Durand', email: 'charlie@xefi.fr', role: 'Utilisateur', actif: false }
-];
+/* Rôles disponibles côté backend */
+const ROLES = ['Admin', 'Superviseur', 'Consultant'];
export default function Users() {
- const [users, setUsers] = useState(mockUsers);
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [erreur, setErreur] = useState('');
- function handleAdd() {
- console.log('Ajouter utilisateur');
+ /* Modale active : 'create' | 'edit' | 'reset' | null */
+ const [modal, setModal] = useState(null);
+ const [selected, setSelected] = useState(null);
+ const [form, setForm] = useState({});
+ const [formError, setFormError] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+
+ const charger = useCallback(async () => {
+ setLoading(true);
+ setErreur('');
+ try {
+ const data = await getUsers();
+ setUsers(Array.isArray(data) ? data : []);
+ } catch (e) {
+ setErreur(e.message || 'Erreur lors du chargement des utilisateurs.');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ charger();
+ }, [charger]);
+
+ /* Ouverture des modales */
+ function ouvrirCreation() {
+ setSelected(null);
+ setForm({ username: '', email: '', password: '', role: 'Consultant' });
+ setFormError('');
+ setModal('create');
}
- function handleEdit(user) {
- console.log('Modifier utilisateur', user);
+ function ouvrirEdition(user) {
+ setSelected(user);
+ setForm({ email: user.email ?? '', role: user.role ?? 'Consultant', actif: !!user.actif });
+ setFormError('');
+ setModal('edit');
}
- function handleDelete(user) {
- setUsers(prev => prev.map(u => u.id === user.id ? { ...u, actif: false } : u));
+ function ouvrirReset(user) {
+ setSelected(user);
+ setForm({ password: '' });
+ setFormError('');
+ setModal('reset');
+ }
+
+ function fermerModale() {
+ setModal(null);
+ setSelected(null);
+ setForm({});
+ setFormError('');
+ }
+
+ /* Suppression (soft delete) avec confirmation */
+ async function handleDelete(user) {
+ if (!window.confirm(`Désactiver l'utilisateur "${user.username}" ?`)) {
+ return;
+ }
+ try {
+ await deleteUser(user.id_user);
+ await charger();
+ } catch (e) {
+ setErreur(e.message || 'Erreur lors de la suppression.');
+ }
+ }
+
+ /* Soumission de la modale courante */
+ async function handleSubmit(e) {
+ e.preventDefault();
+ setFormError('');
+ setSubmitting(true);
+ try {
+ if (modal === 'create') {
+ await createUser({
+ username: form.username,
+ email: form.email,
+ password: form.password,
+ role: form.role,
+ });
+ } else if (modal === 'edit') {
+ await updateUser(selected.id_user, {
+ email: form.email,
+ role: form.role,
+ actif: form.actif,
+ });
+ } else if (modal === 'reset') {
+ await resetUserPassword(selected.id_user, form.password);
+ }
+ fermerModale();
+ await charger();
+ } catch (e) {
+ setFormError(e.message || 'Une erreur est survenue.');
+ } finally {
+ setSubmitting(false);
+ }
}
const columns = [
- { key: 'nom', label: 'Nom' },
+ { key: 'username', label: 'Identifiant' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Rôle' },
{
key: 'actif',
label: 'Statut',
- render: (val) => val ? 'Actif' : 'Inactif'
+ render: (val) => (val ? 'Actif' : 'Inactif'),
},
{
key: 'actions',
label: 'Actions',
render: (val, row) => (
-
- )
- }
+ ),
+ },
];
return (
-
+ {loading ? (
+
+ ) : erreur ? (
+
+ ) : (
+
({ ...u, id: u.id_user }))}
+ pageSize={50}
+ emptyMessage="Aucun utilisateur trouvé."
+ />
+ )}
+
+ {modal && (
+
+
e.stopPropagation()}>
+
+ {modal === 'create' && 'Nouvel utilisateur'}
+ {modal === 'edit' && `Modifier ${selected?.username}`}
+ {modal === 'reset' && `Réinitialiser le mot de passe de ${selected?.username}`}
+
+
+
+
+
+ )}
);
-}
\ No newline at end of file
+}
diff --git a/DataSentinel/src/pages/Users/Users.module.css b/DataSentinel/src/pages/Users/Users.module.css
index d8d1f99..0552da1 100644
--- a/DataSentinel/src/pages/Users/Users.module.css
+++ b/DataSentinel/src/pages/Users/Users.module.css
@@ -40,4 +40,90 @@
.actionBtn:hover {
color: var(--color-primary);
+}
+
+/* Modale de création / édition / réinitialisation */
+.overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 1100;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+}
+
+.modal {
+ background-color: var(--color-white);
+ width: 100%;
+ max-width: 440px;
+ border-radius: var(--radius);
+ border-top: 4px solid var(--color-primary);
+ padding: 28px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+}
+
+.modalTitle {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--color-text);
+ margin: 0 0 20px;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.label {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+.input {
+ padding: 10px 12px;
+ border: 1px solid var(--color-gray-border);
+ border-radius: var(--radius);
+ font-size: 14px;
+ color: var(--color-text);
+ outline: none;
+ transition: border-color 0.15s;
+}
+
+.input:focus {
+ border-color: var(--color-primary);
+}
+
+.checkboxField {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: var(--color-text);
+}
+
+.formError {
+ font-size: 13px;
+ color: var(--color-primary);
+ background-color: var(--color-danger-bg);
+ border-left: 3px solid var(--color-primary);
+ padding: 8px 12px;
+ border-radius: var(--radius);
+ margin: 0;
+}
+
+.modalActions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ margin-top: 8px;
}
\ No newline at end of file
diff --git a/DataSentinel/src/services/api.js b/DataSentinel/src/services/api.js
index c7442ce..7c6f0c9 100644
--- a/DataSentinel/src/services/api.js
+++ b/DataSentinel/src/services/api.js
@@ -3,7 +3,44 @@
/* Utilise la variable d'environnement Vite si définie, sinon localhost par défaut */
const BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8000';
-/* Fonction utilitaire pour les requêtes fetch avec gestion d'erreur */
+/* Clés de stockage local pour le token JWT et l'utilisateur connecté */
+const TOKEN_KEY = 'ds_token';
+const USER_KEY = 'ds_user';
+
+/* Helpers de gestion du token JWT dans le localStorage */
+export function getToken() {
+ return localStorage.getItem(TOKEN_KEY);
+}
+
+export function setToken(token) {
+ localStorage.setItem(TOKEN_KEY, token);
+}
+
+export function clearToken() {
+ localStorage.removeItem(TOKEN_KEY);
+}
+
+/* Construit les en-têtes d'authentification si un token est présent */
+function authHeaders(extra = {}) {
+ const token = getToken();
+ const headers = { ...extra };
+ if (token) {
+ headers.Authorization = `Bearer ${token}`;
+ }
+ return headers;
+}
+
+/* Gestion d'une réponse 401 : purge de la session et redirection vers /login */
+function handleUnauthorized() {
+ clearToken();
+ localStorage.removeItem(USER_KEY);
+ /* Évite une boucle de redirection si on est déjà sur la page de connexion */
+ if (window.location.pathname !== '/login') {
+ window.location.href = '/login';
+ }
+}
+
+/* Fonction utilitaire pour les requêtes fetch (GET) avec gestion d'erreur et auth */
async function fetchApi(path, params = {}) {
const url = new URL(`${BASE_URL}${path}`);
Object.entries(params).forEach(([key, value]) => {
@@ -13,7 +50,11 @@ async function fetchApi(path, params = {}) {
});
try {
- const response = await fetch(url.toString());
+ const response = await fetch(url.toString(), { headers: authHeaders() });
+ if (response.status === 401) {
+ handleUnauthorized();
+ throw new Error('Session expirée, veuillez vous reconnecter.');
+ }
if (!response.ok) {
const errorText = await response.text();
console.error(`Erreur API [${response.status}] ${path}: ${errorText}`);
@@ -26,6 +67,107 @@ async function fetchApi(path, params = {}) {
}
}
+/* Helper générique pour les requêtes JSON (POST / PUT / DELETE) avec auth */
+async function sendJson(path, method, body) {
+ const url = `${BASE_URL}${path}`;
+ const options = {
+ method,
+ headers: authHeaders(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
+ };
+ if (body !== undefined) {
+ options.body = JSON.stringify(body);
+ }
+
+ try {
+ const response = await fetch(url, options);
+ if (response.status === 401) {
+ handleUnauthorized();
+ throw new Error('Session expirée, veuillez vous reconnecter.');
+ }
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error(`Erreur API [${response.status}] ${path}: ${errorText}`);
+ throw new Error(`Erreur ${response.status}: ${errorText}`);
+ }
+ /* Certaines réponses (204, DELETE) peuvent ne pas contenir de corps JSON */
+ const text = await response.text();
+ return text ? JSON.parse(text) : null;
+ } catch (error) {
+ console.error(`Erreur lors de l'appel API ${path}:`, error);
+ throw error;
+ }
+}
+
+/* Authentification : POST /auth/login en x-www-form-urlencoded (OAuth2) */
+export async function login(username, password) {
+ const body = new URLSearchParams();
+ body.append('username', username);
+ body.append('password', password);
+
+ const response = await fetch(`${BASE_URL}/auth/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: body.toString(),
+ });
+
+ if (response.status === 401) {
+ throw new Error('Identifiants invalides');
+ }
+ if (response.status === 429) {
+ throw new Error('Trop de tentatives. Veuillez réessayer dans une minute.');
+ }
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Erreur ${response.status}: ${errorText}`);
+ }
+
+ const data = await response.json();
+ setToken(data.access_token);
+ return data.user;
+}
+
+/* Récupère le profil de l'utilisateur connecté */
+export async function getMe() {
+ return fetchApi('/auth/me');
+}
+
+/* ---------- Administration des utilisateurs ---------- */
+
+export async function getUsers() {
+ return fetchApi('/admin/users');
+}
+
+export async function createUser({ username, email, password, role }) {
+ return sendJson('/admin/users', 'POST', { username, email, password, role });
+}
+
+export async function updateUser(id_user, changes) {
+ return sendJson(`/admin/users/${id_user}`, 'PUT', changes);
+}
+
+export async function deleteUser(id_user) {
+ return sendJson(`/admin/users/${id_user}`, 'DELETE');
+}
+
+export async function resetUserPassword(id_user, password) {
+ return sendJson(`/admin/users/${id_user}/reset-password`, 'POST', { password });
+}
+
+/* Journal d'audit */
+export async function getJournal(limit = 200) {
+ return fetchApi('/admin/journal', { limit });
+}
+
+/* ---------- RGPD : données personnelles ---------- */
+
+export async function exportMyData() {
+ return fetchApi('/me/data-export');
+}
+
+export async function deleteMyAccount() {
+ return sendJson('/me', 'DELETE');
+}
+
/* Santé de l'API */
export async function getHealth() {
return fetchApi('/health');