Framework CRUD/RBAC par convention pour FastAPI, inspiré de Rivet : Router / Controller / Repository plutôt que des endpoints libres.
Inclus : authentification, RBAC deny-by-default, MFA (TOTP, email, WebAuthn) avec codes de secours, reset de mot de passe, stockage de fichiers, planificateur de tâches, générateur de CRUD, audit, i18n et mailing. Testé sur SQLite et PostgreSQL.
Venant de Rivet : docs/RIVET.md liste les
différences.
- Démarrage
- Format des réponses
- Auth et RBAC
- MFA
- Reset de mot de passe
- CRUD
- Scoping utilisateur
- Stockage de fichiers
- Tâches planifiées
- Logs et audit
- i18n
- Mailing
- Configuration
- Migrations
- Tests et CI
- Déploiement
- Structure
make up
make seed # migrations, permissions, utilisateur admin/changeme (rôle ADMIN)| Service | URL |
|---|---|
| API | http://localhost:8000 |
| Swagger (bouton "Authorize" pour coller un token Bearer) | http://localhost:8000/docs |
| Redoc | http://localhost:8000/redoc |
| Schéma OpenAPI | http://localhost:8000/openapi.json |
Adminer (dev uniquement) : système PostgreSQL, serveur db, forge/forge/forge |
http://localhost:8081 |
| Mailpit : capture tous les emails, rien ne part vers un vrai serveur | http://localhost:8025 |
PostgreSQL est exposé sur localhost:5432 (forge/forge/forge).
Le service api monte le code en volume avec --reload : les
modifications sont prises en compte sans rebuild.
pip install -r requirements-dev.txt
uvicorn example_app.main:app --reload
python scripts/seed.pySQLite (./forge.db) par défaut ; DATABASE_URL pour une autre base.
bruno/ est une collection Bruno prête à
l'emploi : ouvrir le dossier, choisir l'environnement local, lancer
Auth/Login (le token est stocké et réutilisé par les autres
requêtes). Les dossiers couvrent auth, MFA, CRUD, fichiers, ainsi que
des cas d'erreur volontaires (filtre non autorisé, suppression en masse
non confirmée, permission manquante).
Toute réponse JSON est enveloppée :
{"data": {"id": 1, "title": "Dune"}, "meta": {"status": "success", "code": 200, "duration_ms": 3.1}}
{"errors": {"detail": "Not found"}, "meta": {"status": "error", "code": 404, "duration_ms": 0.4}}Seul le contenu binaire des fichiers n'est pas enveloppé. Détail,
pagination comprise : docs/RESPONSES.md.
POST /auth/login {login, password} renvoie un token Bearer.
GET /auth/me et POST /auth/logout l'utilisent. /auth/login est
limité à 5 tentatives par minute et par IP (en mémoire : à remplacer
par un backend partagé en multi-instance, voir
forge/security/rate_limit.py).
Chaque route déclare sa politique d'accès : permission="XXX",
public=True ou authenticated_only=True. Une route sans décision
empêche l'application de démarrer (RouteAuthorizationError) : aucune
route n'est ouverte par oubli.
Une permission déclarée dans le code doit exister en base pour être attribuée.
make permissions # crée les permissions déclarées par les routes
make grant role=ADMIN permission=BOOKS_DELETE # attribue à un rôle
make create-role uid=EDITOR name="Éditeur" permissions=BOOKS_DELETE
make create-user login=carol email=carol@exemple.fr role=EDITORSans password=, un mot de passe est généré et affiché une seule fois.
scripts/create_user.py --file users.csv crée plusieurs utilisateurs
(colonnes login,email,password,role). L'attribution aux rôles reste
toujours manuelle ; make seed donne au rôle ADMIN les permissions
existantes à ce moment-là.
Identifiants de l'admin de seed : FORGE_ADMIN_LOGIN,
FORGE_ADMIN_EMAIL, FORGE_ADMIN_PASSWORD (obligatoires en
production).
Méthodes : totp, email, webauthn (FORGE_MFA_METHODS). Une
méthode de chaque type par utilisateur.
| Route | Rôle |
|---|---|
POST /auth/mfa/setup {method, secret?} |
Démarre l'inscription. totp : {secret, qr_uri} (secret optionnel pour importer un token matériel). email : envoie un code. webauthn : {webauthn_options} pour navigator.credentials.create(). |
POST /auth/mfa/confirm {method, code} ou {method: "webauthn", webauthn_response} |
Confirme la méthode. À la première méthode confirmée, renvoie aussi les codes de secours. |
GET /auth/mfa/methods |
{available, enabled, recovery_codes_remaining} |
POST /auth/mfa/request-code {pending_token?} |
Renvoie un code email (connecté, ou en cours de login). |
DELETE /auth/mfa/{method} |
Désactive une méthode. |
Une fois une méthode confirmée, POST /auth/login renvoie
{pending_token, intent: "verify", methods, recovery} au lieu d'un
token (le code email part automatiquement). Puis :
- TOTP / email :
POST /auth/mfa/verify{pending_token, method, code}. - WebAuthn :
POST /auth/mfa/webauthn/login-options{pending_token}→ options pournavigator.credentials.get(), puis/verifyavecwebauthn_response. - Code de secours :
/verifyavecmethod: "recovery".
/verify renvoie le token d'accès. Rate-limité comme /auth/login.
Avec FORGE_MFA_FORCE_ENROLLMENT=1, un compte sans méthode confirmée
reçoit {pending_token, intent: "enroll", methods} au login. Le client
appelle setup puis confirm avec ce pending_token dans le body (sans
Bearer) ; la confirmation renvoie directement le token d'accès.
Codes à usage unique pour se connecter sans la méthode habituelle.
- Remis une seule fois, dans la réponse de
confirmà la première méthode confirmée (recovery_codes). Seul leur hash est stocké. - Format
XXXXX-XXXXX, sans caractères ambigus ; casse, espaces et tirets tolérés à la saisie. - Un code utilisé est marqué et ne fonctionne plus. Un code faux compte
dans
FORGE_MFA_MAX_ATTEMPTS. - Chaque utilisation envoie un email (codes restants, alerte sous
FORGE_MFA_RECOVERY_LOW_THRESHOLD) et est tracée dansactivity_log. - Régénération :
POST /auth/mfa/recovery-codes{password}. Le lot précédent est invalidé. - Ce n'est pas une méthode : ils ne satisfont pas l'inscription forcée et sont supprimés quand plus aucune méthode n'est confirmée.
Pour un utilisateur ayant perdu son moyen d'authentification.
GET /auth/mfa/users/{user_id}→{user_id, enabled, pending}DELETE /auth/mfa/users/{user_id}[?method=totp]→{user_id, removed}
Supprime les méthodes, codes email, pending tokens et codes de secours.
Permission FORGE_MFA_RESET_PERMISSION (défaut MFA_RESET). Tracé
dans activity_log (mfa_reset). Un administrateur ne peut pas cibler
son propre compte.
- Secret TOTP chiffré au repos avec
FORGE_APP_KEY. Changer cette clé rend les secrets existants indéchiffrables. - Anti-rejeu TOTP : un code déjà accepté est refusé, même encore dans sa fenêtre de validité.
- WebAuthn : le compteur de signatures doit progresser à chaque connexion (détection de clé clonée).
FORGE_MFA_MAX_ATTEMPTS(défaut 5) : au-delà, lepending_tokenest détruit, indépendamment du rate-limit par IP.- Sous inscription forcée, impossible de désactiver sa dernière méthode,
sauf permission
FORGE_MFA_FORCE_DISABLE_PERMISSION. FORGE_MFA_BYPASS_PERMISSIONdispense du MFA les rôles qui la détiennent.
FORGE_WEBAUTHN_RP_ID est le domaine exact vu par le navigateur (sans
port ni schéma, localhost en dev) ; FORGE_WEBAUTHN_ORIGIN l'URL
complète. Un décalage fait échouer toute vérification.
Page de test avec une vraie clé : http://localhost:8000/webauthn-test
(dev uniquement, à retirer en production). Les tests utilisent un
authentificateur logiciel (tests/webauthn_helpers.py).
POST /auth/pwd/forgot{email}: réponse identique que le compte existe ou non. Envoie un lien vers le front (FORGE_FRONTEND_URL+FORGE_FRONTEND_PWD_RESET_PATH).POST /auth/pwd/reset{token, password}(8 caractères minimum).
- Un seul lien actif par compte, à usage unique, expiration
FORGE_PWD_RESET_TOKEN_TTL_MINUTES(défaut 60). - Token haché en base.
- Sessions ouvertes révoquées au reset
(
FORGE_PWD_RESET_REVOKES_SESSIONS, défaut activé). - Le reset ne connecte pas et ne contourne pas le MFA.
- Liens expirés purgés chaque nuit par le planificateur (
make pruneà la demande).
books_router = ControllerRouter(BookController, prefix="/books", tags=["books"])
books_router.resource(permissions={"remove": "BOOKS_DELETE", "mass_remove": "BOOKS_DELETE"})resource() enregistre list, show, add, edit, remove et leurs
variantes en masse. Filtres, tri et relations par query string
(?filters=, ?sort=, ?with=), avec liste blanche par Repository :
docs/FILTERS.md.
make crud name=tag fields="name:str,price:float?,author:fk:authors"
make crud name=category table=categories # depuis une table existanteGénère modèle, Repository, Controller et câble la route dans
routes.py (accès : connexion requise).
- Types :
str,text,int,float,bool,datetime;?pour nullable ;champ:fk:table[?]pour une relation N-1,champ:m2m:tablepour une relation N-N (table d'association générée). - Sans
fields=, saisie interactive. - Nom de table déduit au pluriel anglais ;
table=pour un pluriel irrégulier. - La migration reste à générer :
make migration name="add tags"puismake migrate.
Le code généré est du code Forge standard, à modifier librement.
Construction à la main, pas à pas : docs/INTEGRATION.md.
Repository.owner_column = "user_id" restreint list/show/edit/delete
aux lignes de l'utilisateur connecté et assigne l'owner à la création.
Jamais implicite. Exemple : example_app/repositories/note.py.
Pour une vue non scopée, câbler l'action list_all sur une route
dédiée avec une permission (GET /notes/all, permission
NOTES_ADMIN).
Chaque fichier appartient à l'utilisateur qui l'a envoyé.
| Route | Rôle |
|---|---|
POST /files/ (multipart, champ file) |
Upload direct |
GET /files/ |
Liste |
GET /files/{id} |
Upload en cours : statut JSON. Terminé : fichier en ligne (inline). |
GET /files/{id}/download |
Téléchargement forcé (attachment) |
DELETE /files/{id} |
Suppression |
Types autorisés : FORGE_STORAGE_ALLOWED_TYPES (images, PDF, Word,
Excel, texte/CSV par défaut). Taille max : FORGE_STORAGE_MAX_BYTES
(25 Mo). Le nom sur disque est un UUID, jamais le nom envoyé par le
client.
Pour reprendre un gros envoi après coupure :
POST /files/start{filename, content_type, total_bytes}→{id, ...}PUT /files/{id}avec un morceau en corps brut, autant de fois que nécessaire, dans l'ordre.GET /files/{id}donnereceived_bytespour reprendre.POST /files/{id}/completequand tout est reçu.
Taille max d'un chunk : FORGE_STORAGE_CHUNK_SIZE (1 Mo, renvoyée
dans max_chunk_size).
Déclarées dans example_app/schedule.py :
schedule = Schedule(get_settings().scheduler_timezone)
schedule.task(
"prune-password-reset-tokens",
"0 3 * * *",
password_reset.purge_expired,
"Supprime les liens de reset expirés",
)Cron à 5 champs (listes, intervalles, pas, alias @daily etc.),
validé au démarrage. Fuseau : FORGE_SCHEDULER_TIMEZONE (UTC par
défaut).
make schedule-list # tâches et prochaine exécution
make schedule-run # exécute les tâches dues, puis sortLe conteneur scheduler exécute python scripts/schedule.py work en
continu. Une seule instance. Sans processus permanent, appeler
python scripts/schedule.py run chaque minute (cron système,
CronJob Kubernetes).
- Une tâche en erreur est loguée sans bloquer les autres.
- Une tâche encore en cours à sa prochaine échéance est ignorée.
- La minute en cours est sautée au démarrage.
SIGTERMattend la fin des tâches en cours.
Logs applicatifs : logging.getLogger("forge.<module>").
FORGE_LOG_BACKEND=file (défaut) écrit dans storage/logs/forge.log
avec rotation ; mongo écrit dans une collection MongoDB. Toute
exception non gérée y est consignée ; le client reçoit un 500
générique.
Audit : Repository.loggable = True enregistre create/update/delete
dans activity_log (modèle, id, action, auteur, champs modifiés). Les
actions MFA sensibles y sont aussi tracées. Structure :
docs/SCHEMA.md.
forge/i18n/locales/*.json, résolu par trans(key, locale). Locale
issue de Accept-Language, repli sur FORGE_DEFAULT_LOCALE puis sur
la clé.
BaseMail : layout commun (templates/layout.html, couleur
FORGE_BRAND_COLOR) et un template par email, Jinja2, envoi SMTP
asynchrone. En dev, Mailpit capture tout.
Toute la configuration passe par forge/config.py
(pydantic-settings), lue depuis .env (voir .env.example). Les
variables d'environnement l'emportent sur le fichier.
make migrate # applique les migrations
make migration name="add isbn" # génère depuis les modèles
make migrate-down # annule la dernièreLe schéma vient exclusivement d'Alembic. alembic/env.py lit
DATABASE_URL depuis forge.config.
make test # ou hors Docker : pytestBase dédiée recréée avant chaque test (SQLite test.db, ou
TEST_DATABASE_URL), jamais celle de DATABASE_URL : make test ne
touche pas la base de dev. Emails interceptés. Les
tests couvrent l'ensemble des modules, MFA compris (vrais codes TOTP,
signatures WebAuthn réelles).
CI (.github/workflows/ci.yml), à chaque push et pull request :
- lint :
pyflakes. - test-sqlite : la suite sur SQLite.
- test-postgres : migrations (
upgrade head,alembic check,downgrade base) puis la suite sur PostgreSQL. - docker-build :
DockerfileetDockerfile.prod.
Dockerfile.prod (deux étapes, utilisateur non root, sans
dépendances de test) et docker-compose.prod.yml (sans bind-mount ni
outils de dev). Secrets, HTTPS, limites : docs/DEPLOYMENT.md.
Avant de déployer : définir FORGE_APP_KEY, retirer Adminer et la page
/webauthn-test.
forge/
routing.py ControllerRouter, politique d'accès obligatoire par route
controllers/base.py BaseController
repositories/base.py CRUD générique, filtres, tri, relations
query_context.py contexte de requête (filtres, tri, includes)
middleware/ enveloppe de réponse, locale, query string
errors.py exceptions vers réponses JSON
db.py, models/base.py SQLAlchemy async
security/
models.py User, Role, Permission, AccessToken, MFA, reset
auth_controller.py login, logout, me, reset de mot de passe
mfa.py TOTP, codes email, pending tokens
mfa_controller.py routes MFA, administration MFA
recovery.py codes de secours
webauthn.py WebAuthn / FIDO2
crypto.py chiffrement des secrets TOTP
password_reset.py liens de reset
passwords.py, tokens.py bcrypt, tokens opaques hachés
dependencies.py get_current_user, require_permission
rate_limit.py limitation en mémoire
routes.py routeurs auth et MFA
storage/ fichiers, upload par chunks
scheduler.py planificateur
generator/ générateur de CRUD
audit.py activity_log
logs.py configuration des logs
i18n/ traductions
mail/ BaseMail et templates
config.py configuration centrale
example_app/ application d'exemple (Book, Author, Note)
alembic/ migrations
scripts/ seed, permissions, rôles, utilisateurs, planificateur, make:crud
tests/ suite pytest
bruno/ collection HTTP
docs/ FILTERS, RESPONSES, SCHEMA, INTEGRATION, DEPLOYMENT, RIVET
MIT