feat: Gestion des utilisateurs (invitation, blocage, déblocage)
Permet aux administrateurs d'un établissement de gérer le cycle de vie des comptes utilisateurs : inviter de nouveaux membres, bloquer/débloquer des comptes actifs, et renvoyer des invitations en attente. Chaque mutation vérifie l'appartenance au tenant courant pour empêcher les accès cross-tenant. Le blocage est restreint aux comptes actifs uniquement et un administrateur ne peut pas bloquer son propre compte. Les comptes suspendus reçoivent une erreur 403 spécifique au login (sans déclencher l'escalade du rate limiting) et les tentatives sont tracées dans les métriques Prometheus.
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
}
|
||||
|
||||
// Determine which admin section is active
|
||||
const isUsersActive = $derived(page.url.pathname.startsWith('/admin/users'));
|
||||
const isClassesActive = $derived(page.url.pathname.startsWith('/admin/classes'));
|
||||
const isSubjectsActive = $derived(page.url.pathname.startsWith('/admin/subjects'));
|
||||
const isPeriodsActive = $derived(page.url.pathname.startsWith('/admin/academic-year/periods'));
|
||||
@@ -38,6 +39,7 @@
|
||||
</button>
|
||||
<nav class="header-nav">
|
||||
<a href="/dashboard" class="nav-link">Tableau de bord</a>
|
||||
<a href="/admin/users" class="nav-link" class:active={isUsersActive}>Utilisateurs</a>
|
||||
<a href="/admin/classes" class="nav-link" class:active={isClassesActive}>Classes</a>
|
||||
<a href="/admin/subjects" class="nav-link" class:active={isSubjectsActive}>Matières</a>
|
||||
<a href="/admin/academic-year/periods" class="nav-link" class:active={isPeriodsActive}>Périodes</a>
|
||||
|
||||
1214
frontend/src/routes/admin/users/+page.svelte
Normal file
1214
frontend/src/routes/admin/users/+page.svelte
Normal file
File diff suppressed because it is too large
Load Diff
@@ -104,7 +104,7 @@
|
||||
|
||||
try {
|
||||
const result: LoginResult = await login({
|
||||
email,
|
||||
email: email.trim(),
|
||||
password,
|
||||
captcha_token: captchaToken ?? undefined
|
||||
});
|
||||
@@ -188,7 +188,7 @@
|
||||
<h1>Connexion</h1>
|
||||
|
||||
{#if error}
|
||||
<div class="error-banner" class:rate-limited={isRateLimited}>
|
||||
<div class="error-banner" class:rate-limited={isRateLimited} class:account-suspended={error.type === 'account_suspended'}>
|
||||
{#if isRateLimited}
|
||||
<span class="error-icon">🔒</span>
|
||||
<div class="error-content">
|
||||
@@ -197,6 +197,11 @@
|
||||
Réessayez dans <strong>{formatCountdown(retryAfterSeconds)}</strong>
|
||||
</span>
|
||||
</div>
|
||||
{:else if error.type === 'account_suspended'}
|
||||
<span class="error-icon">🚫</span>
|
||||
<div class="error-content">
|
||||
<span class="error-message">{error.message}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="error-icon">⚠</span>
|
||||
<span class="error-message">{error.message}</span>
|
||||
@@ -208,6 +213,7 @@
|
||||
<div class="form-group">
|
||||
<label for="email">Adresse email</label>
|
||||
<div class="input-wrapper">
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
@@ -216,6 +222,7 @@
|
||||
bind:value={email}
|
||||
disabled={isSubmitting || isRateLimited || isDelayed}
|
||||
autocomplete="email"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -393,6 +400,12 @@
|
||||
color: hsl(38, 92%, 30%);
|
||||
}
|
||||
|
||||
.error-banner.account-suspended {
|
||||
background: linear-gradient(135deg, hsl(220, 30%, 95%) 0%, hsl(220, 30%, 97%) 100%);
|
||||
border-color: hsl(220, 30%, 80%);
|
||||
color: hsl(220, 30%, 30%);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
|
||||
Reference in New Issue
Block a user