# Authentification & rôles ## Login Page : `/login` L'identifiant + mot de passe sont vérifiés contre `data/auth.yaml` (mot de passe haché avec **bcrypt**). Format `auth.yaml` : ```yaml credentials: usernames: prof.demo: password: "$2b$12$..." name: "Prof Demo" role: "admin" # ou "user" avatar_url: "/avatars/prof_demo.png" totp_secret: "ABCD1234..." # rempli automatiquement à la 1ère 2FA ``` ## 2FA TOTP (obligatoire) À la **première connexion** d'un nouvel utilisateur : 1. Login + mot de passe corrects 2. L'app génère un secret TOTP et affiche un **QR code** 3. L'utilisateur scanne avec Google Authenticator / Authy / 1Password / etc. 4. Il saisit le code à 6 chiffres pour confirmer 5. Le secret est sauvé dans `auth.yaml` Aux connexions suivantes : 1. Login + mot de passe → demande directe du code TOTP 2. Code à 6 chiffres → connexion finalisée Le code est valide ±30s (paramètre `valid_window=1` de `pyotp`) pour tolérer la dérive d'horloge. ## Session persistante L'authentification est stockée dans le **`localStorage` du navigateur** (champs `username`, `name`, `role`, `photo_url`). La session survit aux rechargements de page et aux redémarrages du conteneur. À chaque page protégée, `AuthState.check_auth` re-vérifie que l'utilisateur existe toujours dans `auth.yaml` ; sinon, redirection forcée vers `/login`. ## Rôles | Page | user | admin | |-------------------|------|-------| | `/accueil` | ✅ | ✅ | | `/fiche` | ✅ | ✅ | | `/classe` | ✅ | ✅ | | `/doc` | ✅ | ✅ | | `/escada` | ❌ | ✅ | | `/cron` | ❌ | ✅ | | `/logs` | ❌ | ✅ | | `/users` | ❌ | ✅ | | `/params` | ❌ | ✅ | ## Gestion des utilisateurs Page `/users` (admin) : - Créer / supprimer des utilisateurs - Changer le rôle - Réinitialiser le 2FA (efface `totp_secret` → forcera une nouvelle config au prochain login) - Définir / changer un avatar ## Logout Bouton "Déconnexion" en bas de la sidebar. Vide le `localStorage` et redirige vers `/login`. ## Stockage des avatars Les fichiers sont sous `assets/avatars/`. Le chemin est référencé dans `auth.yaml` via `avatar_url`. Si `avatar_url` est vide, l'app affiche les initiales de `name`.