Première des deux grandes fonctionnalités demandées (authentification d'abord, export HTML/CSS/JS autonome ensuite) : - Inscription (nom, prénom, email UNIQUE, mot de passe) avec schéma visuel du mot de passe (jauge + liste de critères qui passent au vert en direct — auth/password_strength.py, mêmes règles vérifiées côté serveur qu'affichées côté client). - Double authentification (TOTP, compatible Google Authenticator/Authy) OBLIGATOIRE dès l'inscription : QR code (SVG, sans dépendance Pillow) à scanner puis code à confirmer avant que le compte soit utilisable — voir auth/create_user.py (totp_confirmed) et routes/auth/register_2fa.py. - Connexion en 2 temps (mot de passe puis code TOTP), déconnexion. - Isolation par utilisateur : un compte "user" est limité à un SEUL projet, dont le dossier est nommé d'après son adresse email (slugifiée) et créé automatiquement dès la 2FA confirmée — aucune page de gestion multi-jeux pour lui (redirigé directement vers son propre tableau de bord). Le rôle "admin" reste illimité, comme le moteur l'a toujours été (le TOUT PREMIER compte jamais créé sur une base de comptes vide devient automatiquement admin — voir auth/is_first_user.py — pas de mot de passe par défaut à faire circuler : s'inscrire en premier suffit). Un compte "user" ne peut pas non plus supprimer son unique projet (aucune façon d'en recréer un ensuite). - Garde d'accès globale (core/auth_guard.py, un seul before_request) : toute page exige une connexion, sans avoir touché individuellement aux ~80 routes déjà existantes du moteur. tests/conftest.py isole complètement les tests de la vraie base de comptes (FORGE_USERS_DB_PATH/FORGE_SECRET_KEY_PATH vers un dossier temporaire propre à la session de tests) et authentifie automatiquement la fixture `client` partagée en tant que compte admin de test — les 155 tests déjà existants continuent de passer SANS AUCUNE modification de leur côté, exactement comme avant l'authentification. 10 nouveaux tests dédiés (tests/test_auth.py) : inscription/mots de passe/2FA/connexion/ isolation par projet/blocage de suppression, vérifiés en conditions réelles (vrai client de test Flask, vraie base SQLite, vrais codes TOTP calculés avec pyotp). 165 tests au total, tous au vert. Nouvelles dépendances : pyotp, qrcode (requirements.txt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
54 lines
2.2 KiB
Python
54 lines
2.2 KiB
Python
import re
|
|
|
|
import pyotp
|
|
from werkzeug.security import generate_password_hash
|
|
|
|
from .ensure_schema import ensure_users_schema
|
|
from .connection import connect
|
|
from .get_user_by_email import get_user_by_email
|
|
from .is_first_user import is_first_user
|
|
from .password_strength import password_strength
|
|
|
|
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
|
|
|
|
class UserCreationError(Exception):
|
|
"""Erreur de validation lisible par un humain (affichée telle quelle
|
|
dans le formulaire d'inscription) — jamais un détail SQL/technique."""
|
|
|
|
|
|
def create_user(email, password, nom, prenom):
|
|
"""Crée un compte : mot de passe fort (auth/password_strength.py) et
|
|
2FA (TOTP) rendus obligatoires — le secret est généré ici mais
|
|
totp_confirmed reste à 0 tant que confirm_totp() n'a pas vérifié un
|
|
premier code réellement scanné/tapé (voir routes/auth/register_2fa.py),
|
|
pour ne jamais activer un compte sur un secret QR jamais réellement
|
|
configuré. Le TOUT PREMIER compte créé (base vide) devient
|
|
automatiquement admin (voir is_first_user.py) : pas de mot de passe
|
|
par défaut à faire circuler, juste "inscris-toi en premier"."""
|
|
email = (email or "").strip().lower()
|
|
nom = (nom or "").strip()
|
|
prenom = (prenom or "").strip()
|
|
if not _EMAIL_RE.match(email):
|
|
raise UserCreationError("Adresse email invalide.")
|
|
if not nom or not prenom:
|
|
raise UserCreationError("Nom et prénom sont requis.")
|
|
if get_user_by_email(email):
|
|
raise UserCreationError("Un compte existe déjà avec cette adresse email.")
|
|
if not password_strength(password)["valid"]:
|
|
raise UserCreationError("Mot de passe trop faible — respecte les critères affichés.")
|
|
|
|
ensure_users_schema()
|
|
role = "admin" if is_first_user() else "user"
|
|
totp_secret = pyotp.random_base32()
|
|
conn = connect()
|
|
conn.execute(
|
|
"""INSERT INTO _users (email, password_hash, nom, prenom, role, totp_secret, totp_confirmed)
|
|
VALUES (?, ?, ?, ?, ?, ?, 0)""",
|
|
(email, generate_password_hash(password), nom, prenom, role, totp_secret),
|
|
)
|
|
user_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
|
conn.commit()
|
|
conn.close()
|
|
return user_id
|