Files
Forge-Engine/auth/ensure_schema.py
T
williamandClaude Sonnet 5 d5a84413d1 Ajoute la réinitialisation de mot de passe par email (SMTP)
Un lien "Mot de passe oublié ?" (login.html) mène à /forgot-password :
si l'adresse saisie correspond à un compte, un jeton à haute entropie
(secrets.token_urlsafe, valable 1h) est généré et envoyé par email via
smtplib (auth/send_email.py, aucune dépendance ajoutée) — configuré
uniquement par variables d'environnement (SMTP_HOST/PORT/USER/PASSWORD/
FROM, voir .env.example et docker-compose.prod.yml), n'importe quel
serveur SMTP existant convient (Mailcow compris).

Seul le hash SHA-256 du jeton est stocké (auth/password_reset.py, table
_password_reset_tokens) : un jeton envoyé par email reste inutilisable
même en cas de fuite de la base. Le même message générique s'affiche que
l'adresse corresponde à un compte ou non, pour ne jamais permettre à ce
formulaire de servir à deviner quelles adresses sont déjà inscrites. Un
échec d'envoi (SMTP non configuré) est journalisé côté serveur seulement,
jamais révélé à l'utilisateur.

/reset-password/<token> vérifie le jeton (non expiré, non déjà utilisé),
applique les mêmes règles de mot de passe fort qu'à l'inscription (même
schéma visuel), puis consomme le jeton et remet à zéro le compteur
anti-bruteforce du compte (auth/set_password.py) — une identité prouvée
par email est une voie de récupération légitime même pour un compte
verrouillé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 20:50:49 +02:00

66 lines
2.6 KiB
Python

from .connection import connect
def ensure_users_schema():
conn = connect()
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS _users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
nom TEXT NOT NULL,
prenom TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
totp_secret TEXT NOT NULL,
totp_confirmed INTEGER NOT NULL DEFAULT 0,
project_slug TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
"""
)
# Anti-bruteforce (voir rate_limit.py) : ajoutées en ALTER TABLE pour
# ne rien casser sur une base de comptes déjà créée avant cette
# fonctionnalité (le compte admin déjà inscrit sur ce poste, par
# exemple) — CREATE TABLE IF NOT EXISTS ci-dessus n'aurait sinon
# aucun effet sur une table qui existe déjà sans ces colonnes.
cols = {r["name"] for r in conn.execute("PRAGMA table_info(_users)").fetchall()}
if "failed_attempts" not in cols:
conn.execute("ALTER TABLE _users ADD COLUMN failed_attempts INTEGER NOT NULL DEFAULT 0")
if "locked_until" not in cols:
conn.execute("ALTER TABLE _users ADD COLUMN locked_until TEXT")
# Codes de récupération 2FA (voir recovery_codes.py) : table séparée
# plutôt qu'une colonne JSON sur _users, pour pouvoir marquer/consommer
# chaque code UN PAR UN (used_at) sans avoir à relire/réécrire toute
# la liste à chaque connexion.
conn.execute(
"""
CREATE TABLE IF NOT EXISTS _recovery_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
code_hash TEXT NOT NULL,
used_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
"""
)
# Jetons de réinitialisation de mot de passe (voir password_reset.py) :
# seul leur hash (SHA-256, un jeton aléatoire à haute entropie n'a pas
# besoin d'un hash de mot de passe volontairement lent) est stocké, un
# jeton envoyé par email reste donc inutilisable même en cas de fuite
# de cette base.
conn.execute(
"""
CREATE TABLE IF NOT EXISTS _password_reset_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
expires_at TEXT NOT NULL,
used_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
"""
)
conn.commit()
conn.close()