feat: Attribution de rôles multiples par utilisateur

Les utilisateurs Classeo étaient limités à un seul rôle, alors que
dans la réalité scolaire un directeur peut aussi être enseignant,
ou un parent peut avoir un rôle vie scolaire. Cette limitation
obligeait à créer des comptes distincts par fonction.

Le modèle User supporte désormais plusieurs rôles simultanés avec
basculement via le header. L'admin peut attribuer/retirer des rôles
depuis l'interface de gestion, avec des garde-fous : pas d'auto-
destitution, pas d'escalade de privilèges (seul SUPER_ADMIN peut
attribuer SUPER_ADMIN), vérification du statut actif pour le
switch de rôle, et TTL explicite sur le cache de rôle actif.
This commit is contained in:
2026-02-10 07:57:43 +01:00
parent 9ccad77bf0
commit e930c505df
93 changed files with 2527 additions and 165 deletions

View File

@@ -0,0 +1,113 @@
<script lang="ts">
import {
getRoles,
getActiveRole,
hasMultipleRoles,
switchTo,
getIsSwitching
} from '$features/roles/roleContext.svelte';
let selectedRole = $state(getActiveRole() ?? '');
let switchError = $state<string | null>(null);
// Sync selected role when active role changes externally
$effect(() => {
const active = getActiveRole();
if (active) {
selectedRole = active;
}
});
async function handleSwitch() {
if (selectedRole === getActiveRole()) return;
switchError = null;
const success = await switchTo(selectedRole);
if (!success) {
switchError = 'Erreur lors du basculement';
selectedRole = getActiveRole() ?? '';
}
}
</script>
{#if hasMultipleRoles()}
<div class="role-switcher">
<label for="role-switcher" class="role-switcher-label">Vue :</label>
<select
id="role-switcher"
bind:value={selectedRole}
onchange={handleSwitch}
disabled={getIsSwitching()}
class="role-switcher-select"
>
{#each getRoles() as role}
<option value={role.value}>{role.label}</option>
{/each}
</select>
{#if getIsSwitching()}
<span class="role-switcher-spinner"></span>
{/if}
{#if switchError}
<span class="role-switcher-error">{switchError}</span>
{/if}
</div>
{/if}
<style>
.role-switcher {
display: flex;
align-items: center;
gap: 0.5rem;
}
.role-switcher-label {
font-size: 0.75rem;
font-weight: 500;
color: var(--text-secondary, #64748b);
white-space: nowrap;
}
.role-switcher-select {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
font-weight: 500;
color: var(--accent-primary, #0ea5e9);
background: var(--accent-primary-light, #e0f2fe);
border: 1px solid var(--accent-primary, #0ea5e9);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
appearance: auto;
}
.role-switcher-select:hover:not(:disabled) {
background: var(--accent-primary, #0ea5e9);
color: white;
}
.role-switcher-select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.role-switcher-spinner {
width: 14px;
height: 14px;
border: 2px solid var(--border-subtle, #e2e8f0);
border-top-color: var(--accent-primary, #0ea5e9);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.role-switcher-error {
font-size: 0.75rem;
color: var(--color-alert, #ef4444);
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>