# Authentification, droits & profil ## 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$..." # bcrypt name: "Prof Demo" role: "admin" # ou "user" email: "prof.demo@eptm.ch" # destinataire pour reset mdp / enrôlement avatar_url: "/avatars/prof_demo.png" totp_secret: "ABCD1234..." # rempli automatiquement à la 1ère 2FA allowed_classes: ["AUTOMAT 1", "EM-AU 2"] # restriction d'accès escada_username: "prenom.nom@eptm.ch" # email Escada (clé login) escada_password: "..." # mot de passe Escada (stocké clair) ``` ## 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 ±30 s (paramètre `valid_window=1` de `pyotp`) pour tolérer la dérive d'horloge. ## Email de bienvenue / réinitialisation de mot de passe Sur création d'un user depuis `/users` ou sur reset, un **email** est envoyé contenant un lien `/password_set?token=...` qui expire après 24 h. - L'URL de base est lue depuis `settings.app_base_url` (configurée en `/params → Application`). - L'expéditeur et le SMTP sont configurés en `/params → Configuration email`. ## Session persistante L'authentification est stockée dans le **`localStorage` du navigateur** (champs `username`, `name`, `role`, `photo_url`, `theme`). 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` et rafraîchit `allowed_classes`, `escada_username`, `escada_has_password`, etc. ; sinon, redirection forcée vers `/login`. ## Droits d'accès par classe Chaque user a une clé `allowed_classes: list[str] | null` : - **`null` ou absente** → restriction non encore appliquée (user nouveau) - **`[]` vide** → aucun accès → popup d'enrôlement obligatoire (cf. ci-dessous) - **liste non vide** → l'user ne voit que ces classes dans /classe, /fiche, et les filtres de stats sur /accueil - **role=admin** → voit tout (bypass de la restriction) `src/user_access.py:get_allowed_classes(username)` est l'API canonique. Toutes les pages user-facing l'appellent pour filtrer. ## Self-service enrôlement (popup obligatoire) Quand un user se connecte et que `allowed_classes` est `[]` (ou non défini), un **dialog forcé** s'ouvre sur toutes les pages : > **Configurez votre accès** > > Pour récupérer la liste des classes auxquelles vous avez accès dans Escadaweb, > saisissez vos identifiants Escada + un code TOTP courant. Champs : - Email Escada (= `escada_username`) - Mot de passe Escada (stocké clair dans `auth.yaml`) - Code 2FA courant (utilisé une fois — non stocké) À la soumission, le script `scripts/fetch_user_classes.py` est lancé **en arrière-plan** (`@rx.event(background=True)` + `asyncio.create_subprocess_exec`) pour ne pas bloquer l'app pour les autres users : 1. Playwright headless lance un profil temporaire isolé 2. Login Keycloak avec les creds + TOTP fourni 3. Scrape la liste des classes accessibles dans Escada 4. Filtre MP / MI / classes « Formation » 5. Sauvegarde dans `auth.yaml` : `allowed_classes`, `escada_username`, `escada_password` 6. Log live dans `operations.log` (préfixe `[fetch_classes:]`) → visible dans `/logs` Le popup peut être fermé avec « Plus tard » (`enroll_dismissed=True`), il réapparaîtra au prochain login. ## Page « Mon profil » (`/profile`) Accessible via le popover sidebar : - Avatar - Liste actuelle des `allowed_classes` - Bouton « Relancer la synchronisation » pour rafraîchir la liste (même script que le popup) - Modifier les identifiants Escada (sans relancer la sync immédiatement) ## Réinitialisation des droits (admin) Sur la page `/users`, un bouton « Réinitialiser les droits » (par user) : - Efface `allowed_classes` - Efface `escada_username` + `escada_password` → Au prochain login de cet user, le popup d'enrôlement réapparaît. ## Rôles | Page | user (sans allowed_classes) | user (avec allowed_classes) | admin | |-------------------|----------------------------|------------------------------|-------| | `/accueil` | ✅ (filtré) | ✅ (filtré) | ✅ | | `/classe` | ✅ (filtré) | ✅ (filtré) | ✅ | | `/fiche` | ✅ (filtré) | ✅ (filtré) | ✅ | | `/doc` | ✅ | ✅ | ✅ | | `/profile` | ✅ | ✅ | ✅ | | `/escada` | ❌ | ❌ | ✅ | | `/cron` | ❌ | ❌ | ✅ | | `/logs` | ❌ | ❌ | ✅ | | `/users` | ❌ | ❌ | ✅ | | `/params` | ❌ | ❌ | ✅ | | `/feedback` | ❌ | ❌ | ✅ | ## Gestion des utilisateurs (admin) Page `/users` : - Créer / supprimer des utilisateurs - Changer le rôle - Réinitialiser le 2FA (efface `totp_secret` → forcera une nouvelle config au prochain login) - Réinitialiser les droits (cf. ci-dessus) - Définir / changer un avatar - Clic sur une ligne ouvre directement le panneau d'édition ## Logout Bouton « Déconnexion » dans le popover profil de la sidebar. Vide le `localStorage` (y compris `enroll_dismissed`) 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`.