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

@@ -5,12 +5,33 @@
import DashboardTeacher from '$lib/components/organisms/Dashboard/DashboardTeacher.svelte';
import DashboardStudent from '$lib/components/organisms/Dashboard/DashboardStudent.svelte';
import DashboardAdmin from '$lib/components/organisms/Dashboard/DashboardAdmin.svelte';
import { getActiveRole, getIsLoading } from '$features/roles/roleContext.svelte';
type UserRole = 'parent' | 'teacher' | 'student' | 'admin' | 'direction';
type DashboardView = 'parent' | 'teacher' | 'student' | 'admin';
// For now, default to parent role with demo data
// TODO: Fetch real user profile from /api/me when endpoint is implemented
let userRole = $state<UserRole>('parent');
const ROLE_TO_VIEW: Record<string, DashboardView> = {
ROLE_PARENT: 'parent',
ROLE_PROF: 'teacher',
ROLE_ELEVE: 'student',
ROLE_ADMIN: 'admin',
ROLE_SUPER_ADMIN: 'admin',
ROLE_VIE_SCOLAIRE: 'admin',
ROLE_SECRETARIAT: 'admin'
};
// Fallback demo role when not authenticated or roles not loaded
let demoRole = $state<DashboardView>('parent');
// Use real role context if available, otherwise fallback to demo
let dashboardView = $derived<DashboardView>(
ROLE_TO_VIEW[getActiveRole() ?? ''] ?? demoRole
);
// True when roles come from the API (user is authenticated)
let hasRoleContext = $derived(getActiveRole() !== null);
// True when role loading has started (indicates an authenticated user)
let isRoleLoading = $derived(getIsLoading());
// Simulated first login detection (in real app, this comes from API)
let isFirstLogin = $state(true);
@@ -26,16 +47,13 @@
function handleToggleSerenity(enabled: boolean) {
serenityEnabled = enabled;
// TODO: POST to /api/me/preferences when backend is ready
console.log('Serenity score preference updated:', enabled);
}
// Cast demo data to proper type
const typedDemoData = demoData as DemoData;
// Allow switching roles for demo purposes
function switchRole(role: UserRole) {
userRole = role;
function switchDemoRole(role: DashboardView) {
demoRole = role;
isFirstLogin = false;
}
</script>
@@ -44,17 +62,25 @@
<title>Tableau de bord - Classeo</title>
</svelte:head>
<!-- Demo role switcher - TODO: Remove when real authentication is implemented -->
<!-- This will be hidden once we can determine user role from /api/me -->
<div class="demo-controls">
<span class="demo-label">Démo - Changer de rôle :</span>
<button class:active={userRole === 'parent'} onclick={() => switchRole('parent')}>Parent</button>
<button class:active={userRole === 'teacher'} onclick={() => switchRole('teacher')}>Enseignant</button>
<button class:active={userRole === 'student'} onclick={() => switchRole('student')}>Élève</button>
<button class:active={userRole === 'admin'} onclick={() => switchRole('admin')}>Admin</button>
</div>
<!-- Loading state when roles are being fetched -->
{#if isRoleLoading}
<div class="loading-state">
<div class="spinner"></div>
<p>Chargement du tableau de bord...</p>
</div>
{:else if !hasRoleContext && !isRoleLoading}
<!-- Demo role switcher shown when not authenticated (no role context from API) -->
<!-- The RoleSwitcher in the header handles multi-role switching for authenticated users -->
<div class="demo-controls">
<span class="demo-label">Démo - Changer de rôle :</span>
<button class:active={demoRole === 'parent'} onclick={() => switchDemoRole('parent')}>Parent</button>
<button class:active={demoRole === 'teacher'} onclick={() => switchDemoRole('teacher')}>Enseignant</button>
<button class:active={demoRole === 'student'} onclick={() => switchDemoRole('student')}>Élève</button>
<button class:active={demoRole === 'admin'} onclick={() => switchDemoRole('admin')}>Admin</button>
</div>
{/if}
{#if userRole === 'parent'}
{#if dashboardView === 'parent'}
<DashboardParent
demoData={typedDemoData}
{isFirstLogin}
@@ -64,11 +90,11 @@
{childName}
onToggleSerenity={handleToggleSerenity}
/>
{:else if userRole === 'teacher'}
{:else if dashboardView === 'teacher'}
<DashboardTeacher isLoading={false} {hasRealData} />
{:else if userRole === 'student'}
{:else if dashboardView === 'student'}
<DashboardStudent demoData={typedDemoData} isLoading={false} {hasRealData} isMinor={true} />
{:else if userRole === 'admin' || userRole === 'direction'}
{:else if dashboardView === 'admin'}
<DashboardAdmin
isLoading={false}
{hasRealData}
@@ -77,6 +103,30 @@
{/if}
<style>
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
text-align: center;
}
.spinner {
width: 2rem;
height: 2rem;
border: 3px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.demo-controls {
display: flex;
align-items: center;