"use client"; // Sécurité : gestion des comptes (rôle, cours, désactivation, réinitialisation du mot // de passe, révocation des sessions) et journal des événements d'authentification. import { useState } from "react"; import { Check, Copy, KeyRound, LogOut, ShieldAlert, Users } from "lucide-react"; import { PageHeader } from "@/components/app-shell"; import { Badge, Button, Card, EmptyState, Modal, Skeleton, Spinner, cn } from "@/components/ui"; import { ErrorBanner, SectionTitle, fmtDate, fmtInt, postJson, useFetchJson } from "./shared"; type UserRow = { id: number; username: string; display_name: string; email: string | null; role: "student" | "instructor" | "admin"; disabled: number; must_change_password: number; is_initial_admin: number; created_at: string; last_login_at: string | null; courses: string | null; // "IMM1003,IMM1033" sessions: number; }; type AuthEvent = { id: number; user_id: number | null; username: string | null; event: string; ip: string | null; detail: string | null; created_at: string; }; const ROLE_LABEL: Record = { student: "Étudiant·e", instructor: "Professeur", admin: "Admin", }; const COURSES = ["IMM1003", "IMM1033"] as const; function eventTone(event: string): string { if (/failed|rate-limited|disabled/.test(event)) return "text-red-600 dark:text-red-400 font-medium"; if (event === "login-success" || event === "register") return "text-emerald-700 dark:text-emerald-400"; return "text-fg"; } export function AdminSecurity() { const { data, error, loading, reload } = useFetchJson<{ users: UserRow[]; events: AuthEvent[] }>("/api/admin/users"); const [actionError, setActionError] = useState(null); const [busyId, setBusyId] = useState(null); // Réinitialisation du mot de passe const [resetTarget, setResetTarget] = useState(null); const [tempPassword, setTempPassword] = useState(null); const [copied, setCopied] = useState(false); async function patch(userId: number, body: Record): Promise<{ ok: boolean; tempPassword: string | null } | null> { setBusyId(userId); setActionError(null); try { const r = await postJson<{ ok: boolean; tempPassword: string | null }>("/api/admin/users", { userId, ...body }, "PATCH"); await reload(); return r; } catch (e) { setActionError(e instanceof Error ? e.message : "L'action a échoué."); return null; } finally { setBusyId(null); } } function toggleCourse(u: UserRow, course: string) { const current = (u.courses ?? "").split(",").filter(Boolean); const next = current.includes(course) ? current.filter((c) => c !== course) : [...current, course]; patch(u.id, { courses: next }); } async function resetPassword() { if (!resetTarget) return; const r = await patch(resetTarget.id, { resetPassword: true }); if (r?.tempPassword) { setTempPassword(r.tempPassword); setCopied(false); } } async function copyPassword() { if (!tempPassword) return; try { await navigator.clipboard.writeText(tempPassword); setCopied(true); setTimeout(() => setCopied(false), 2500); } catch { setActionError("Impossible de copier — sélectionnez le mot de passe manuellement."); } } function closeResetModal() { setResetTarget(null); setTempPassword(null); setCopied(false); } return (
{actionError &&
} {loading ? (
) : error || !data ? ( ) : ( <> {/* Utilisateurs */} {data.users.length === 0 ? ( } title="Aucun utilisateur" description="Aucun compte n'existe encore." /> ) : (
{data.users.map((u) => ( ))}
Identifiant Nom Rôle Cours Sessions Dernière connexion Actions
{u.username} {!!u.is_initial_admin && admin initial} {!!u.disabled && désactivé}
{u.display_name || "—"}
{COURSES.map((c) => { const enrolled = (u.courses ?? "").split(",").includes(c); return ( ); })}
{fmtInt(u.sessions)} {u.last_login_at ? fmtDate(u.last_login_at) : "Jamais"}
{busyId === u.id && }
)} {/* Journal d'authentification */}
Journal des événements d'authentification {data.events.length === 0 ? (

Aucun événement enregistré.

) : (
{data.events.map((e) => ( ))}
Événement Utilisateur IP Date
{e.event} {e.username ?? "—"} {e.ip ?? "—"} {fmtDate(e.created_at)}
)}
)} {/* Modal de réinitialisation */} {!tempPassword ? ( <>

Réinitialiser le mot de passe de {resetTarget?.username} ?

Un mot de passe temporaire sera généré, toutes ses sessions seront fermées et la personne devra choisir un nouveau mot de passe à sa prochaine connexion.

) : ( <>

Mot de passe temporaire pour {resetTarget?.username} :

{tempPassword}
Ce mot de passe ne sera plus jamais affiché. Transmettez-le de façon sécurisée (en personne ou par un canal chiffré) — jamais par courriel en clair. La personne devra le remplacer dès sa première connexion.
)}
); }