feat(auth): real JWT login, RBAC routes, RGPD cookie banner + users/journal admin
Build & Deploy / build (push) Successful in 11s
Build & Deploy / build (push) Successful in 11s
This commit is contained in:
@@ -30,7 +30,7 @@ export default function Journal() {
|
||||
|
||||
const columns = [
|
||||
{ key: 'date_action', label: 'Date' },
|
||||
{ key: 'utilisateur', label: 'Utilisateur' },
|
||||
{ key: 'username', label: 'Utilisateur' },
|
||||
{ key: 'action', label: 'Action' },
|
||||
{ key: 'detail', label: 'Détail' },
|
||||
{ key: 'ip', label: 'Adresse IP' },
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
/* Page de connexion : formulaire + SSO Windows */
|
||||
/* Page de connexion : formulaire avec authentification JWT + captcha anti-bot */
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { getHealth } from '../../services/api';
|
||||
import styles from './Login.module.css';
|
||||
|
||||
/* Génère deux entiers aléatoires pour le captcha additif */
|
||||
function genererCaptcha() {
|
||||
return {
|
||||
x: Math.floor(Math.random() * 10) + 1,
|
||||
y: Math.floor(Math.random() * 10) + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const [identifiant, setIdentifiant] = useState('');
|
||||
const [motDePasse, setMotDePasse] = useState('');
|
||||
const [captcha, setCaptcha] = useState(() => genererCaptcha());
|
||||
const [reponseCaptcha, setReponseCaptcha] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [erreur, setErreur] = useState('');
|
||||
|
||||
@@ -18,25 +27,29 @@ export default function Login() {
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setErreur('');
|
||||
setLoading(true);
|
||||
|
||||
/* Vérification anti-bot du captcha avant tout appel réseau */
|
||||
if (parseInt(reponseCaptcha, 10) !== captcha.x + captcha.y) {
|
||||
setErreur('Réponse au captcha incorrecte.');
|
||||
setCaptcha(genererCaptcha());
|
||||
setReponseCaptcha('');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
/* Vérification de la disponibilité de l'API avant connexion */
|
||||
await getHealth();
|
||||
login({ nom: identifiant || 'Utilisateur', role: 'user', token: 'session' });
|
||||
await login(identifiant, motDePasse);
|
||||
navigate('/dashboard');
|
||||
} catch {
|
||||
setErreur('Impossible de contacter le serveur. Vérifiez que l\'API est démarrée sur le port 8000.');
|
||||
} catch (err) {
|
||||
/* Message générique en cas d'identifiants invalides ou d'erreur serveur */
|
||||
setErreur(err.message === 'Identifiants invalides' ? 'Identifiants invalides' : (err.message || 'Identifiants invalides'));
|
||||
setCaptcha(genererCaptcha());
|
||||
setReponseCaptcha('');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSso() {
|
||||
/* Connexion SSO Windows : même vérification API */
|
||||
handleSubmit({ preventDefault: () => {} });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.card}>
|
||||
@@ -73,6 +86,21 @@ export default function Login() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor="captcha">
|
||||
Vérification : combien font {captcha.x} + {captcha.y} ?
|
||||
</label>
|
||||
<input
|
||||
id="captcha"
|
||||
type="number"
|
||||
className={styles.input}
|
||||
value={reponseCaptcha}
|
||||
onChange={e => setReponseCaptcha(e.target.value)}
|
||||
placeholder="Votre réponse"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{erreur && <p className={styles.erreur}>{erreur}</p>}
|
||||
|
||||
<button type="submit" className={styles.btnPrimary} disabled={loading}>
|
||||
@@ -80,14 +108,6 @@ export default function Login() {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className={styles.divider}>
|
||||
<span>ou</span>
|
||||
</div>
|
||||
|
||||
<button type="button" className={styles.btnSecondary} onClick={handleSso} disabled={loading}>
|
||||
Connexion Windows SSO
|
||||
</button>
|
||||
|
||||
<div className={styles.links}>
|
||||
<a href="#" className={styles.forgotLink}>Mot de passe oublié ?</a>
|
||||
</div>
|
||||
|
||||
@@ -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) => (
|
||||
<div className={styles.actions}>
|
||||
<button className={styles.actionBtn} onClick={() => handleEdit(row)}>
|
||||
<button className={styles.actionBtn} onClick={() => ouvrirEdition(row)}>
|
||||
<Edit size={14} />
|
||||
Modifier
|
||||
</button>
|
||||
<button className={styles.actionBtn} onClick={() => ouvrirReset(row)}>
|
||||
<KeyRound size={14} />
|
||||
Mot de passe
|
||||
</button>
|
||||
<button className={styles.actionBtn} onClick={() => handleDelete(row)}>
|
||||
<Trash2 size={14} />
|
||||
Désactiver
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>Utilisateurs</h1>
|
||||
<Button variant="primary" onClick={handleAdd}>
|
||||
<Button variant="primary" onClick={ouvrirCreation}>
|
||||
<Plus size={14} />
|
||||
Ajouter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={users}
|
||||
pageSize={50}
|
||||
emptyMessage="Aucun utilisateur trouvé."
|
||||
/>
|
||||
{loading ? (
|
||||
<LoadingSpinner message="Chargement des utilisateurs..." />
|
||||
) : erreur ? (
|
||||
<ErrorMessage message={erreur} onRetry={charger} />
|
||||
) : (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={users.map(u => ({ ...u, id: u.id_user }))}
|
||||
pageSize={50}
|
||||
emptyMessage="Aucun utilisateur trouvé."
|
||||
/>
|
||||
)}
|
||||
|
||||
{modal && (
|
||||
<div className={styles.overlay} role="dialog" aria-modal="true" onClick={fermerModale}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<h2 className={styles.modalTitle}>
|
||||
{modal === 'create' && 'Nouvel utilisateur'}
|
||||
{modal === 'edit' && `Modifier ${selected?.username}`}
|
||||
{modal === 'reset' && `Réinitialiser le mot de passe de ${selected?.username}`}
|
||||
</h2>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSubmit}>
|
||||
{modal === 'create' && (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor="username">Identifiant</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={form.username}
|
||||
onChange={e => setForm(f => ({ ...f, username: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(modal === 'create' || modal === 'edit') && (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
className={styles.input}
|
||||
value={form.email}
|
||||
onChange={e => setForm(f => ({ ...f, email: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(modal === 'create' || modal === 'reset') && (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor="password">Mot de passe</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
className={styles.input}
|
||||
value={form.password}
|
||||
onChange={e => setForm(f => ({ ...f, password: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(modal === 'create' || modal === 'edit') && (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor="role">Rôle</label>
|
||||
<select
|
||||
id="role"
|
||||
className={styles.input}
|
||||
value={form.role}
|
||||
onChange={e => setForm(f => ({ ...f, role: e.target.value }))}
|
||||
>
|
||||
{ROLES.map(r => (
|
||||
<option key={r} value={r}>{r}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modal === 'edit' && (
|
||||
<div className={styles.checkboxField}>
|
||||
<input
|
||||
id="actif"
|
||||
type="checkbox"
|
||||
checked={form.actif}
|
||||
onChange={e => setForm(f => ({ ...f, actif: e.target.checked }))}
|
||||
/>
|
||||
<label htmlFor="actif">Compte actif</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formError && <p className={styles.formError}>{formError}</p>}
|
||||
|
||||
<div className={styles.modalActions}>
|
||||
<Button variant="neutral" type="button" onClick={fermerModale} disabled={submitting}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button variant="primary" type="submit" disabled={submitting}>
|
||||
{submitting ? 'Enregistrement...' : 'Enregistrer'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user