docs: les deux documents d exploitation qui manquaient
DOC-001 demandait quatre documents courts. Deux existaient — installation (deployment.md) et restauration (database-restore.md). Les deux autres n existaient pas. docs/roles-and-permissions.md decrit le comportement **implemente**, pas celui qu on souhaiterait, et note les endroits ou les deux divergent : un gerant gere toutes les equipes mais pas toutes les selections, asymetrie presente depuis toujours et ecrite nulle part ailleurs. Le piege du double rattachement coach/equipe y est en toutes lettres, avec le motif a ne jamais reintroduire. Le document dit aussi qui a raison en cas de desaccord : les tests. Une documentation d autorisation qui se contredit avec le code est pire que pas de documentation, parce qu on la croit. docs/incident-runbook.md part du symptome, pas du composant. Le reflexe d ouverture est la reference affichee sur la page d erreur — c est ce que l identifiant de requete rend possible, et sans un endroit qui le dise, la fonctionnalite ne sert a personne. Trois affirmations ont ete verifiees contre le code avant d etre ecrites, et deux etaient fausses : le corps de /health en echec dit database="error" et non "disconnected", et les evenements de verrouillage s appellent login.failure et account.throttled. La liste complete des quatorze evenements de auth.log est maintenant dans le manuel. README : index des documents, et deux points d etat corriges — TRUSTED_PROXY est desormais une variable, et l absence de validation des inscriptions par le staff est nommee comme decision en attente.
This commit is contained in:
@@ -94,14 +94,31 @@ Ce qui **n'est pas** fait, pour que personne ne s'y fie :
|
|||||||
à un modèle est absente de la production.
|
à un modèle est absente de la production.
|
||||||
- **Les secrets de l'historique git ne sont pas révoqués** — jeton du bot,
|
- **Les secrets de l'historique git ne sont pas révoqués** — jeton du bot,
|
||||||
mot de passe PostgreSQL, `SECRET_KEY`.
|
mot de passe PostgreSQL, `SECRET_KEY`.
|
||||||
- **`trusted_proxy='*'`** dans `wsgi.py` : l'en-tête `X-Forwarded-For` est
|
- **`trusted_proxy='*'`** reste le défaut : l'en-tête `X-Forwarded-For` est
|
||||||
accepté de n'importe quelle source, donc la limitation par IP est
|
accepté de n'importe quelle source, donc la limitation par IP est
|
||||||
contournable. À régler avec la topologie réelle du déploiement.
|
contournable. C'est désormais la variable `TRUSTED_PROXY` plutôt qu'une
|
||||||
|
constante — `docs/deployment.md` donne la valeur pour chaque topologie.
|
||||||
|
- **Les comptes créés par le formulaire d'inscription sont actifs
|
||||||
|
immédiatement** : il n'y a pas d'étape de validation par le staff. Décision
|
||||||
|
de produit en attente, voir `docs/roles-and-permissions.md`.
|
||||||
- **L'identité Discord** transite encore par un champ caché du formulaire
|
- **L'identité Discord** transite encore par un champ caché du formulaire
|
||||||
d'inscription : elle n'est pas prouvée par le passage OAuth2.
|
d'inscription : elle n'est pas prouvée par le passage OAuth2.
|
||||||
|
|
||||||
`docs/security-checklist.md` détaille la liste avant mise en production.
|
`docs/security-checklist.md` détaille la liste avant mise en production.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
| Document | Pour |
|
||||||
|
|---|---|
|
||||||
|
| `docs/deployment.md` | Installer, déployer, revenir en arrière |
|
||||||
|
| `docs/roles-and-permissions.md` | Qui peut faire quoi, et où c'est décidé |
|
||||||
|
| `docs/database-restore.md` | Sauvegarder et restaurer |
|
||||||
|
| `docs/database-schema.md` | Sortir de `create_all()` : relevé, Alembic, migrations |
|
||||||
|
| `docs/incident-runbook.md` | Quand quelque chose ne va pas |
|
||||||
|
| `docs/architecture.md` | Diagrammes |
|
||||||
|
| `docs/translations.md` | Ajouter ou corriger une traduction |
|
||||||
|
| `docs/security-checklist.md` | Avant une mise en production |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Intégration Discord
|
## Intégration Discord
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Manuel d'incident
|
||||||
|
|
||||||
|
À lire quand quelque chose ne va pas. Chaque section : le symptôme, où
|
||||||
|
regarder, quoi faire.
|
||||||
|
|
||||||
|
> **Le réflexe préalable** — demander la **référence** affichée sur la page
|
||||||
|
> d'erreur (`Référence à indiquer si vous signalez ce problème`). C'est
|
||||||
|
> l'identifiant de requête ; il apparaît dans chaque ligne de journal produite
|
||||||
|
> par cette requête, entre crochets.
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> grep '\[a1b2c3d4e5f60718\]' logs/*.log
|
||||||
|
> ```
|
||||||
|
|
||||||
|
## Où sont les journaux
|
||||||
|
|
||||||
|
| Fichier | Contenu |
|
||||||
|
|---|---|
|
||||||
|
| `logs/app.log` | Tout, à partir de `LOG_LEVEL` (défaut `INFO`) |
|
||||||
|
| `logs/errors.log` | `ERROR` et au-dessus, avec les traces |
|
||||||
|
| `logs/auth.log` | Connexions, échecs, verrous, changements de rôle, suppressions de compte, refus d'inscription |
|
||||||
|
| Console Pterodactyl | Les mêmes lignes que `app.log` — le handler console est actif en production, délibérément |
|
||||||
|
| `C:\nginx\logs\error.log` | Ce qui n'a jamais atteint l'application |
|
||||||
|
|
||||||
|
Format : `[date] NIVEAU [module:ligne] [id-de-requête] message`. Un `-` à la
|
||||||
|
place de l'identifiant signifie « hors requête » : démarrage, bot Discord,
|
||||||
|
planificateur.
|
||||||
|
|
||||||
|
### Les événements de `auth.log`
|
||||||
|
|
||||||
|
Tout est en `clé=valeur`, dans l'ordre, donc greppable sans dépendance JSON.
|
||||||
|
|
||||||
|
| Événement | Quand |
|
||||||
|
|---|---|
|
||||||
|
| `login.success` | Connexion réussie |
|
||||||
|
| `login.failure` | Mot de passe faux sur un compte existant |
|
||||||
|
| `login.failure.unknown_user` | Identifiant inconnu |
|
||||||
|
| `login.rejected.deactivated` | Bons identifiants, compte désactivé |
|
||||||
|
| `account.throttled` | Délai d'attente déclenché après des échecs répétés |
|
||||||
|
| `logout` | Déconnexion |
|
||||||
|
| `account.registered` | Inscription réussie |
|
||||||
|
| `account.registration_refused` | Inscription filtrée — `reason=honeypot`, `too-fast` ou `no-form-issued` |
|
||||||
|
| `account.created_by_admin` | Compte créé depuis l'administration |
|
||||||
|
| `account.updated` | Fiche modifiée |
|
||||||
|
| `account.role_changed` | Changement de rôle, avec l'ancien et le nouveau |
|
||||||
|
| `account.password_changed` | Changement par la personne elle-même |
|
||||||
|
| `account.password_reset_by_admin` | Réinitialisation par un président |
|
||||||
|
| `account.deleted` | Suppression, avec le nombre de fichiers de contrat retirés |
|
||||||
|
|
||||||
|
Le champ `ip=` vient de `request.remote_addr`, donc de `X-Forwarded-For`.
|
||||||
|
Tant que `TRUSTED_PROXY=*`, **c'est une indication et pas une preuve**.
|
||||||
|
|
||||||
|
## Le site ne répond pas
|
||||||
|
|
||||||
|
1. **`/health` répond-il ?**
|
||||||
|
```bash
|
||||||
|
curl -i https://<hôte>/health
|
||||||
|
```
|
||||||
|
- Pas de réponse du tout → nginx est tombé, ou l'application n'écoute plus.
|
||||||
|
Vérifier la console Pterodactyl.
|
||||||
|
- `503` → l'application tourne mais la base ne répond pas. Voir plus bas.
|
||||||
|
- `200` mais le site est inutilisable → le problème est dans nginx ou dans
|
||||||
|
le DNS, pas dans l'application.
|
||||||
|
|
||||||
|
2. **L'application est-elle démarrée ?** Console Pterodactyl. Au démarrage elle
|
||||||
|
imprime `Starting Waitress server on <hôte>:<port>`.
|
||||||
|
|
||||||
|
3. **Un déploiement vient-il d'avoir lieu ?** C'est la cause la plus fréquente.
|
||||||
|
Voir « Revenir en arrière » dans `docs/deployment.md`.
|
||||||
|
|
||||||
|
## `/health` renvoie 503
|
||||||
|
|
||||||
|
La base ne répond pas. Le corps de la réponse le dit :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"status": "unhealthy", "database": "error"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le corps ne contient **jamais** l'erreur du pilote : elle porte l'hôte, le nom
|
||||||
|
de la base et l'utilisateur de la chaîne de connexion, et `/health` n'est pas
|
||||||
|
authentifié. La trace complète est dans `logs/errors.log`.
|
||||||
|
|
||||||
|
- La base est hébergée sur Render : vérifier son état côté Render en premier.
|
||||||
|
- `DATABASE_URL` a-t-elle changé ? Un mot de passe tourné et pas reporté
|
||||||
|
produit exactement ça.
|
||||||
|
- **Piège connu** : `DATABASE_URL=postgresql://…` seul ne démarre pas —
|
||||||
|
SQLAlchemy y cherche psycopg **2**, le projet épingle psycopg **3**.
|
||||||
|
`normalise_database_url()` nomme le pilote, donc les deux formes marchent ;
|
||||||
|
si vous voyez `ModuleNotFoundError: psycopg2`, c'est que le code qui
|
||||||
|
normalise n'a pas été déployé.
|
||||||
|
|
||||||
|
## Plus aucune notification Discord
|
||||||
|
|
||||||
|
Le bot tourne dans un **fil du même processus** que le site. Quand il meurt,
|
||||||
|
les pages continuent d'être servies et toutes les notifications s'arrêtent.
|
||||||
|
C'est précisément ce qui est resté invisible longtemps.
|
||||||
|
|
||||||
|
1. `/health` le rapporte :
|
||||||
|
```json
|
||||||
|
{"discord_bot": {"running": false, ...}}
|
||||||
|
```
|
||||||
|
Rapporté et **non fatal** : un club sans rappels Discord est dégradé, pas
|
||||||
|
hors service, et un 503 le sortirait du répartiteur de charge pour ça.
|
||||||
|
|
||||||
|
2. Chercher les livraisons manquées :
|
||||||
|
```bash
|
||||||
|
grep 'not delivered' logs/app.log
|
||||||
|
```
|
||||||
|
Trois causes, qui ne se traitent pas pareil :
|
||||||
|
- `their direct messages are closed` → **définitif**. La personne doit
|
||||||
|
autoriser les messages privés des membres du serveur. Réessayer ne sert à
|
||||||
|
rien.
|
||||||
|
- `does not exist` → l'identifiant Discord sur le compte est faux ou le
|
||||||
|
compte a été supprimé. À corriger dans la fiche de la personne.
|
||||||
|
- autre chose → passager, côté Discord.
|
||||||
|
|
||||||
|
3. Le bilan du lot quotidien :
|
||||||
|
```bash
|
||||||
|
grep 'Daily reminders' logs/app.log
|
||||||
|
```
|
||||||
|
`17 of 20 delivered, 3 failed` — les trois sont nommées juste au-dessus.
|
||||||
|
|
||||||
|
4. Redémarrer le processus relance le bot. Les réactions en attente survivent
|
||||||
|
au redémarrage (`discord_pending.json`), sauf si ce fichier a été mis de
|
||||||
|
côté pour corruption — auquel cas le journal le dit et donne le nom du
|
||||||
|
fichier de quarantaine.
|
||||||
|
|
||||||
|
## Quelqu'un est verrouillé dehors
|
||||||
|
|
||||||
|
Cinq échecs consécutifs déclenchent un délai qui double ensuite. Le compte
|
||||||
|
n'est pas bloqué définitivement.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -E 'login\.failure|account\.throttled' logs/auth.log | grep '<identifiant>'
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Le verrou se lève tout seul.** Le délai est dans le message.
|
||||||
|
- Un président peut réinitialiser le mot de passe depuis la fiche du compte,
|
||||||
|
ce qui remet le compteur à zéro.
|
||||||
|
- **Beaucoup de verrous sur des comptes différents en même temps** → quelqu'un
|
||||||
|
essaie des mots de passe. Le champ `ip=` est une indication et **pas une
|
||||||
|
preuve** tant que `TRUSTED_PROXY=*` (voir `OPS-002` dans
|
||||||
|
`docs/deployment.md`) : il est falsifiable à chaque requête.
|
||||||
|
|
||||||
|
## Une page renvoie 500
|
||||||
|
|
||||||
|
1. Récupérer la référence auprès de la personne, ou la dernière trace :
|
||||||
|
```bash
|
||||||
|
tail -50 logs/errors.log
|
||||||
|
```
|
||||||
|
2. La trace complète y est. Elle n'est jamais montrée à l'utilisateur.
|
||||||
|
3. Si c'est arrivé juste après un déploiement, revenir en arrière d'abord et
|
||||||
|
diagnostiquer ensuite.
|
||||||
|
|
||||||
|
## Le style ou le JavaScript ne se chargent plus
|
||||||
|
|
||||||
|
Les statiques sont servis par nginx avec un cache de 30 jours, ce qui n'est
|
||||||
|
sûr que parce que leurs URL portent une estampille (`?v=<mtime>`).
|
||||||
|
|
||||||
|
- **Après un déploiement, la page a l'ancien style** → l'estampille n'a pas
|
||||||
|
changé, donc le fichier n'a pas été déployé. Vérifier :
|
||||||
|
```bash
|
||||||
|
curl -s https://<hôte>/auth/login | grep -o 'style.css?v=[0-9]*'
|
||||||
|
```
|
||||||
|
- **404 sur `/static/…`** → le chemin `alias` du bloc `location /static/` de
|
||||||
|
`nginx.conf` ne pointe pas au bon endroit.
|
||||||
|
- **Une page est cassée et la console du navigateur parle de CSP** → un bloc
|
||||||
|
`<script>` a été ajouté sans `nonce="{{ csp_nonce }}"`. Sans nonce il ne
|
||||||
|
s'exécute pas, et **rien dans les journaux ne le signale**.
|
||||||
|
- **Une ressource de CDN ne charge plus** → une empreinte `integrity` ne
|
||||||
|
correspond plus. Le navigateur refuse le fichier en silence côté serveur.
|
||||||
|
Cas normal : quelqu'un a monté la version sans recalculer l'empreinte.
|
||||||
|
|
||||||
|
## Les données semblent fausses
|
||||||
|
|
||||||
|
**Une colonne existe dans le modèle et pas dans la base.** `create_all()` ne
|
||||||
|
fait jamais d'`ALTER`, donc c'est possible et ce n'est pas rare :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app/supporting_scripts/schema_report.py --url "$DATABASE_URL"
|
||||||
|
```
|
||||||
|
|
||||||
|
Lecture seule. Le rapport dit exactement ce qui manque. Voir
|
||||||
|
`docs/database-schema.md`.
|
||||||
|
|
||||||
|
## Restaurer
|
||||||
|
|
||||||
|
Procédure complète dans `docs/database-restore.md`. Deux règles :
|
||||||
|
|
||||||
|
- restaurer **d'abord sur une copie**, jamais directement par-dessus la
|
||||||
|
production ;
|
||||||
|
- une sauvegarde qu'on n'a jamais restaurée est une hypothèse, pas une
|
||||||
|
sauvegarde.
|
||||||
|
|
||||||
|
## À faire une fois, et pas encore fait
|
||||||
|
|
||||||
|
Ces points sont ouverts et connus. Ils ne sont pas des incidents, ils en
|
||||||
|
causeront.
|
||||||
|
|
||||||
|
| Point | Pourquoi c'est urgent |
|
||||||
|
|---|---|
|
||||||
|
| Révoquer `SECRET_KEY` et le jeton du bot Discord | Les deux sont dans l'historique git des deux dépôts, et dans le HEAD du miroir GitHub. Avec la `SECRET_KEY`, on forge une session valide pour n'importe quel compte |
|
||||||
|
| Vérifier le compte `admin`/`password` | `python app/supporting_scripts/schema_report.py --check-seed-accounts` |
|
||||||
|
| Régler `HOST` et `TRUSTED_PROXY` | Tant que c'est `*`, la limite de débit et le journal d'authentification sont falsifiables |
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Rôles et permissions
|
||||||
|
|
||||||
|
Qui peut faire quoi, et où c'est décidé. Ce document décrit le comportement
|
||||||
|
**implémenté**, pas celui qu'on souhaiterait : quand les deux divergent, c'est
|
||||||
|
noté.
|
||||||
|
|
||||||
|
## Les cinq rôles
|
||||||
|
|
||||||
|
| Rôle | Nom dans le code | En français, dans l'interface |
|
||||||
|
|---|---|---|
|
||||||
|
| Président | `admin` | président |
|
||||||
|
| Gérant | `manager` | gérant |
|
||||||
|
| Coach | `coach` | coach |
|
||||||
|
| Recruteur | `scout` | recruteur |
|
||||||
|
| Joueur | `player` | joueur |
|
||||||
|
|
||||||
|
Le rôle est une **colonne discriminante** : `users.role` décide de la classe
|
||||||
|
Python que SQLAlchemy instancie (`Admin`, `Manager`, …). C'est pourquoi
|
||||||
|
l'autorisation se fait par `isinstance()` et non par comparaison de chaînes,
|
||||||
|
et pourquoi changer un rôle passe par un `UPDATE` explicite suivi d'une
|
||||||
|
relecture de l'instance — voir le long commentaire dans `edit_user`.
|
||||||
|
|
||||||
|
## Capacités globales
|
||||||
|
|
||||||
|
Lues sur l'objet utilisateur. Défaut hérité de `User` : **tout est `False`**,
|
||||||
|
chaque sous-classe ouvre ce qui la concerne. Ajouter un rôle sans rien
|
||||||
|
surcharger donne donc un compte qui ne peut rien, ce qui est le bon sens de
|
||||||
|
défaut.
|
||||||
|
|
||||||
|
| Capacité | Président | Gérant | Coach | Recruteur | Joueur |
|
||||||
|
|---|:--:|:--:|:--:|:--:|:--:|
|
||||||
|
| `can_manage_users` | ✅ | — | — | — | — |
|
||||||
|
| `can_manage_teams` | ✅ | ✅ | — | — | — |
|
||||||
|
| `can_manage_tryouts` | ✅ | ✅ | ✅ | — | — |
|
||||||
|
| `can_schedule_matches` | ✅ | ✅ | ✅ | — | — |
|
||||||
|
| `can_evaluate` | ✅ | ✅ | ✅ | ✅ | — |
|
||||||
|
|
||||||
|
Le recruteur évalue et ne gère rien : c'est le seul rôle en lecture-plus-
|
||||||
|
évaluation.
|
||||||
|
|
||||||
|
## Capacités portant sur un objet
|
||||||
|
|
||||||
|
Les deux qui comptent, parce qu'elles répondent différemment selon l'objet.
|
||||||
|
|
||||||
|
### `can_manage_this_tryout(tryout)`
|
||||||
|
|
||||||
|
| Rôle | Règle |
|
||||||
|
|---|---|
|
||||||
|
| Président | toujours vrai |
|
||||||
|
| Gérant | `tryout.created_by == moi` **ou** `tryout.manager_id == moi` |
|
||||||
|
| Coach | délègue à `coach_manages_tryout()` — voir plus bas |
|
||||||
|
| Recruteur, Joueur | toujours faux |
|
||||||
|
|
||||||
|
Un gérant ne gère donc **pas** toutes les sélections, alors qu'il gère toutes
|
||||||
|
les équipes. L'asymétrie est dans le code depuis toujours ; elle est peut-être
|
||||||
|
voulue, elle n'est écrite nulle part ailleurs qu'ici.
|
||||||
|
|
||||||
|
### `can_manage_this_org_team(org_team)`
|
||||||
|
|
||||||
|
| Rôle | Règle |
|
||||||
|
|---|---|
|
||||||
|
| Président | toujours vrai |
|
||||||
|
| Gérant | **toujours vrai** — toutes les équipes, pas seulement les siennes |
|
||||||
|
| Coach | rattaché à l'équipe, par la relation m2m **ou** par la colonne héritée |
|
||||||
|
| Recruteur, Joueur | toujours faux |
|
||||||
|
|
||||||
|
## Le piège : deux façons d'être rattaché
|
||||||
|
|
||||||
|
C'est le point le plus coûteux de tout le modèle d'autorisation, et il est
|
||||||
|
encore ouvert (`ARCH-001`).
|
||||||
|
|
||||||
|
Un coach est rattaché à une équipe de **deux** manières :
|
||||||
|
|
||||||
|
```
|
||||||
|
OrgTeam.coach_id colonne d'origine, un seul coach
|
||||||
|
OrgTeam.coaches relation many-to-many, ajoutée ensuite
|
||||||
|
```
|
||||||
|
|
||||||
|
**Les deux sont encore alimentées à l'écriture.** Ne lire que `coach_id`
|
||||||
|
excluait silencieusement tout coach qui n'était pas le premier de son équipe ;
|
||||||
|
y ajouter un `.first()` excluait un coach de toutes ses équipes sauf une. Les
|
||||||
|
deux défauts étaient actifs en production.
|
||||||
|
|
||||||
|
Même duplication pour les gérants (`manager_id` / `managers`) et pour les
|
||||||
|
sélections (`Tryout.coach_id` / `Tryout.coaches` / `target_org_team_id`).
|
||||||
|
|
||||||
|
**Règle absolue : ne jamais réintroduire ce motif.**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ jamais
|
||||||
|
OrgTeam.query.filter_by(coach_id=user.id).first()
|
||||||
|
|
||||||
|
# ✅ toujours
|
||||||
|
from app.permissions import coach_org_teams
|
||||||
|
coach_org_teams(user)
|
||||||
|
```
|
||||||
|
|
||||||
|
`app/permissions.py` est **le** point de vérité. Toute question « ce coach
|
||||||
|
peut-il… ? » ou « quelles équipes… ? » passe par lui. Il considère les deux
|
||||||
|
rattachements et toutes les équipes, ce qui rend la duplication inoffensive
|
||||||
|
**en lecture**. L'écriture crée toujours les deux.
|
||||||
|
|
||||||
|
`ARCH-001` fera disparaître les colonnes héritées — migration de données, donc
|
||||||
|
bloqué sur Alembic (`DB-002`, voir `docs/database-schema.md`).
|
||||||
|
|
||||||
|
## Les fonctions à appeler
|
||||||
|
|
||||||
|
Toutes dans `app/permissions.py`, toutes prenant l'utilisateur agissant en
|
||||||
|
argument explicite plutôt que de lire `current_user` — ce qui les rend
|
||||||
|
appelables depuis les modèles, depuis le bot Discord et depuis les tests, sans
|
||||||
|
contexte de requête.
|
||||||
|
|
||||||
|
| Fonction | Répond à |
|
||||||
|
|---|---|
|
||||||
|
| `coach_org_teams(coach)` | Les équipes de ce coach, les deux rattachements confondus |
|
||||||
|
| `manager_org_teams(manager)` | Idem pour un gérant |
|
||||||
|
| `attached_org_teams(user)` | Les équipes auxquelles cette personne est rattachée, quel que soit son rôle |
|
||||||
|
| `visible_org_teams(user)` | Ce qu'elle a le droit de voir |
|
||||||
|
| `can_manage_org_team(user, team)` | Peut-elle administrer cette équipe |
|
||||||
|
| `coach_player_ids(coach)` | Les joueurs qu'il encadre |
|
||||||
|
| `coach_can_access_player(coach, id)` | Peut-il lire la fiche de ce joueur |
|
||||||
|
| `can_manage_player_contract(user, id)` | Peut-elle déposer un contrat pour lui |
|
||||||
|
| `coach_tryouts(coach)` | Les sélections qu'il gère |
|
||||||
|
| `coach_manages_tryout(coach, tryout)` | Gère-t-il celle-ci |
|
||||||
|
|
||||||
|
## Ce que voit chaque rôle
|
||||||
|
|
||||||
|
`get_visible_tryouts()`, sur l'objet utilisateur :
|
||||||
|
|
||||||
|
| Rôle | Sélections visibles |
|
||||||
|
|---|---|
|
||||||
|
| Président | toutes |
|
||||||
|
| Recruteur | toutes |
|
||||||
|
| Gérant | celles qu'il a créées ou dont il est le gérant |
|
||||||
|
| Coach | celles qu'il gère, via `coach_tryouts()` |
|
||||||
|
| Joueur | celles où il est inscrit, plus celles ouvertes à son équipe |
|
||||||
|
|
||||||
|
## Inscription et activation
|
||||||
|
|
||||||
|
**Un compte créé par le formulaire d'inscription est actif immédiatement.**
|
||||||
|
`is_active_account` vaut `True` par défaut ; il n'existe aucune étape de
|
||||||
|
validation par le staff.
|
||||||
|
|
||||||
|
Ce qui filtre les inscriptions aujourd'hui est un champ piège et un délai
|
||||||
|
minimal (`SEC-AUTH-008`, `app/routes/auth.py`), plus une limite de débit de
|
||||||
|
20 par heure et par IP. C'est calibré contre le pourriel de masse, pas contre
|
||||||
|
quelqu'un qui lit la page.
|
||||||
|
|
||||||
|
**C'est une décision de produit en attente** : faut-il que le staff active les
|
||||||
|
comptes ? Tant qu'elle n'est pas prise, n'importe qui peut créer un compte
|
||||||
|
joueur et voir ce qu'un joueur voit.
|
||||||
|
|
||||||
|
## Désactiver un compte
|
||||||
|
|
||||||
|
`is_active_account = False` prend effet **à la requête suivante** :
|
||||||
|
`User.is_active` est surchargé pour le lire, et Flask-Login le consulte à
|
||||||
|
chaque requête. Une session ouverte ne survit donc pas à la désactivation.
|
||||||
|
|
||||||
|
## Le dernier président
|
||||||
|
|
||||||
|
Deux garde-fous, parce qu'aucune interface ne permet de les défaire ensuite :
|
||||||
|
|
||||||
|
- un président ne peut pas changer **son propre** rôle ;
|
||||||
|
- le **dernier président actif** ne peut pas être rétrogradé.
|
||||||
|
|
||||||
|
## Ce qui n'est pas couvert ici
|
||||||
|
|
||||||
|
- La matrice détaillée par domaine fonctionnel de l'audit du 2026-08-07 vit
|
||||||
|
dans `audit/2026-08-07-MATRICE-AUTORISATIONS.md`, non versionné.
|
||||||
|
- Les tests d'autorisation sont dans `tests/test_authorization.py` et
|
||||||
|
`tests/test_permissions.py`. Ils sont la référence exécutable : si ce
|
||||||
|
document et eux divergent, **ce sont eux qui ont raison**.
|
||||||
Reference in New Issue
Block a user