138 lines
6.4 KiB
Markdown
138 lines
6.4 KiB
Markdown
# 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 `<APP_URL>/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:<username>]`) → 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`.
|