Files
Mow910andCursor b274dcaa51 Ajout de fonctionnalités majeures Twitch, Discord et interface web
Nouvelles fonctionnalités :
- Système de modération Twitch complet (bans, timeouts, avertissements)
- Filtre de liens intelligent pour Twitch avec whitelist/blacklist
- Notifications d'événements Twitch (follows, subs, raids, etc.)
- Système Freeloot Discord avec flux RSS dédié
- Salons automatiques Discord (création/suppression dynamique)
- Authentification utilisateur pour l'interface web (login/register)
- Gestion des utilisateurs et permissions
- Interface de modération Twitch dans la webapp
- Interface de gestion des événements Twitch
- Configuration des paramètres utilisateur
- Migration BDD pour les mots bannis

Améliorations de l'interface :
- Refonte complète des templates (configurations, commandes, humeurs, etc.)
- Nouvelle page de settings utilisateur
- Page de gestion des utilisateurs (admin)
- Page d'erreur 403 personnalisée
- Amélioration de la navigation et du design global
- Intégration de nouvelles sections dans le menu principal

Modifications techniques :
- Ajout de nouveaux modèles en base de données
- Extension des helpers database
- Mise à jour des dépendances (requirements.txt)
- Amélioration de la gestion des annonces Twitch
- Refactorisation du code pour meilleure maintenabilité

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 22:37:22 +01:00

165 lines
5.8 KiB
Python

# Authentification webapp : login, register, logout et contrôle d'accès par rôles.
from functools import wraps
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from database import db
from database.models import WebappUser, ROLE_ORDER, PagePermission
from database.helpers import ConfigurationHelper
from webapp import webapp
def require_roles(allowed_roles: list):
"""Décorateur : exige que l'utilisateur soit authentifié et ait l'un des rôles autorisés."""
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for("login", next=request.url))
if current_user.role not in allowed_roles:
return render_template("403.html"), 403
return f(*args, **kwargs)
return wrapped
return decorator
def require_role_min(min_role: str):
"""Décorateur : exige que l'utilisateur ait au moins le rôle min_role (niveau en base)."""
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for("login", next=request.url))
if not current_user.has_role_at_least(min_role):
return render_template("403.html"), 403
return f(*args, **kwargs)
return wrapped
return decorator
def _page_min_level(page_key: str, for_write: bool = False) -> int:
"""Niveau minimum requis pour la page (lecture ou écriture)."""
perm = PagePermission.query.filter_by(page_key=page_key).first()
if not perm:
return 0
if for_write and perm.write_level is not None:
return perm.write_level
return perm.min_level
def require_page(page_key: str):
"""Décorateur : accès en lecture selon les permissions de la page (webapp_page_permission)."""
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for("login", next=request.url))
min_level = _page_min_level(page_key, for_write=False)
if not current_user.has_level_at_least(min_level):
return render_template("403.html"), 403
return f(*args, **kwargs)
return wrapped
return decorator
def require_page_write(page_key: str):
"""Décorateur : accès en écriture selon les permissions de la page."""
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for("login", next=request.url))
min_level = _page_min_level(page_key, for_write=True)
if not current_user.has_level_at_least(min_level):
return render_template("403.html"), 403
return f(*args, **kwargs)
return wrapped
return decorator
def can_write_page(page_key: str) -> bool:
"""Retourne True si l'utilisateur connecté a le niveau pour écrire sur cette page."""
if not current_user.is_authenticated:
return False
return current_user.has_level_at_least(_page_min_level(page_key, for_write=True))
@webapp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("index"))
if request.method == "POST":
identifier = (request.form.get("identifier") or "").strip()
password = request.form.get("password") or ""
if not identifier or not password:
flash("Identifiant et mot de passe requis.", "error")
return render_template("login.html")
user = WebappUser.query.filter(
(WebappUser.username == identifier) | (WebappUser.email == identifier)
).first()
if user and check_password_hash(user.password_hash, password):
login_user(user, remember=True)
next_url = request.args.get("next")
if next_url and next_url.startswith("/"):
return redirect(next_url)
return redirect(url_for("index"))
flash("Identifiant ou mot de passe incorrect.", "error")
return render_template("login.html")
return render_template("login.html")
@webapp.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for("index"))
# Inscriptions désactivées par le super admin
reg_enabled = ConfigurationHelper().getValue("registration_enabled")
if reg_enabled in (None, "", "false", "0", "no", "off"):
flash("Les inscriptions sont désactivées.", "error")
return redirect(url_for("login"))
if request.method == "POST":
username = (request.form.get("username") or "").strip()
email = (request.form.get("email") or "").strip().lower()
password = request.form.get("password") or ""
password_confirm = request.form.get("password_confirm") or ""
errors = []
if len(username) < 3:
errors.append("Le nom d'utilisateur doit faire au moins 3 caractères.")
if len(email) < 5 or "@" not in email:
errors.append("Adresse e-mail invalide.")
if len(password) < 8:
errors.append("Le mot de passe doit faire au moins 8 caractères.")
if password != password_confirm:
errors.append("Les mots de passe ne correspondent pas.")
if WebappUser.query.filter_by(username=username).first():
errors.append("Ce nom d'utilisateur est déjà pris.")
if WebappUser.query.filter_by(email=email).first():
errors.append("Cette adresse e-mail est déjà utilisée.")
if errors:
for msg in errors:
flash(msg, "error")
return render_template("register.html")
# Premier inscrit = super administrateur
role = "super_administrateur" if WebappUser.query.count() == 0 else "viewer_twitch"
user = WebappUser(
username=username,
email=email,
password_hash=generate_password_hash(password, method="scrypt"),
role=role,
)
db.session.add(user)
db.session.commit()
flash("Compte créé. Vous pouvez vous connecter.", "success")
return redirect(url_for("login"))
return render_template("register.html")
@webapp.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("login"))