Nexus / app.py
ernestmindres's picture
Update app.py
dabe992 verified
# app.py
import json
import os
import sys
import io
import uuid
from functools import wraps
from datetime import datetime
from flask import Flask, request, jsonify, Response, session
from flask_cors import CORS
import baserow_storage # Assurez-vous que ceci est présent
# Importation des modules backend
from auth_backend import (
register_user,
login_user,
get_user_by_id,
get_plan_limit,
reset_password_via_security_question,
generate_password_hash,
# Nouvelles fonctions pour la gestion des utilisateurs finaux
register_end_user,
login_end_user,
reset_end_user_password_by_client
)
from decorators import api_key_required # <-- NOUVEL IMPORT
# Importation des Blueprints
from web_routes import web_bp
from user_routes import user_bp
from billing_routes import billing_bp
# Valeur par défaut pour la taille max de contenu
DEFAULT_MAX_CONTENT_LENGTH = 16 * 1024 * 1024
# --- Initialisation de l'Application Flask ---
app = Flask(__name__)
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1)
# ------------------------------------------------------------------
# Configuration
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "super_secret_dev_key")
app.config['MAX_CONTENT_LENGTH'] = DEFAULT_MAX_CONTENT_LENGTH
# Permettre les requêtes cross-origin (CORS)
CORS(app, supports_credentials=True, origins="*", allow_headers=["Content-Type", "X-User-API-Key"])
# Permettre les requêtes cross-origin pour l'API
CORS(app)
# --- Enregistrement des Blueprints (Nouveau) ---
app.register_blueprint(web_bp)
app.register_blueprint(user_bp)
app.register_blueprint(billing_bp) # <-- NOUVEL ENREGISTREMENT
# --- Décorateurs d'Authentification (Conservés) ---
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
# Redirection HTTP 302 vers la page de connexion pour les requêtes non-API
if not request.path.startswith('/api/'):
from flask import redirect, url_for
return redirect(url_for('user_bp.connexion'))
# Réponse JSON pour les API
return jsonify({"status": "Error", "message": "Accès non autorisé. Veuillez vous connecter.", "code": "AUTH_REQUIRED"}), 401
return f(*args, **kwargs)
return decorated_function
def user_api_key_required(f):
"""Décorateur pour exiger la clé API dynamique via URL ('api_key') ou en-tête ('X-User-API-Key')."""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.args.get('api_key') or request.headers.get('X-User-API-Key')
if not api_key:
return jsonify({
"status": "Error",
"message": "Clé API utilisateur ('api_key' dans l'URL ou 'X-User-API-Key' dans l'en-tête) manquante.",
"code": "USER_API_KEY_MISSING"
}), 403
user = get_user_by_api_key(api_key)
if not user:
return jsonify({
"status": "Error",
"message": "Clé API utilisateur invalide.",
"code": "USER_API_KEY_INVALID"
}), 403
request.user_client_id = user['user_id']
request.user_client_data = user
return f(*args, **kwargs)
return decorated_function
# --- Routes d'Authentification (API - Conservées) ---
@app.route("/api/register", methods=["POST"])
def register():
data = request.get_json()
username = data.get("username")
email = data.get("email")
password = data.get("password")
confirm_password = data.get("confirm_password")
security_question = data.get("security_question")
security_answer = data.get("security_answer")
# CORRECTION ICI: Déballage des 3 valeurs retournées par register_user
user_id, message, new_user_data = register_user(username, email, password, confirm_password, security_question, security_answer)
if user_id and new_user_data: # Vérifier l'ID et les données pour s'assurer du succès
session['user_id'] = user_id
# Réponse JSON pour l'API, incluant la clé API
return jsonify({
"message": message,
"status": "Success",
"user_id": user_id,
# On récupère la clé API directement des données utilisateur
"api_key": new_user_data.get("api_key")
}), 201
else:
# Échec de l'inscription (message d'erreur de register_user)
return jsonify({"message": message, "status": "Error"}), 400
@app.route("/api/login", methods=["POST"])
def login():
"""
Route de connexion de l'utilisateur principal.
Prend le nom d'utilisateur/email et le mot de passe.
"""
data = request.get_json()
username = data.get("username")
password = data.get("password")
# CORRECTION DE L'ERREUR :
# Nous déballons maintenant 3 valeurs (ID, Message, Données Utilisateur)
# car la fonction login_user() dans auth_backend.py a été modifiée pour
# retourner les 3 valeurs.
user_id, message, user_data = login_user(username, password)
# Note: user_data est la 3ème valeur (Dict des données utilisateur ou None)
if user_id and user_data:
# La connexion est réussie
session['user_id'] = user_id
# Réponse API avec la clé API de l'utilisateur pour les futures requêtes
return jsonify({
"message": message,
"status": "Success",
"user_id": user_id,
# On utilise les données utilisateur (user_data) que nous avons déjà récupérées
"api_key": user_data.get("api_key")
}), 200
else:
# La connexion a échoué (identifiants invalides ou autre erreur)
return jsonify({"message": message, "status": "Error"}), 401
@app.route("/api/logout", methods=["POST"])
def logout():
session.pop('user_id', None)
return jsonify({"message": "Déconnexion réussie.", "status": "Success"}), 200
@app.route("/api/forgot-password", methods=["POST"])
def forgot_password_api(): # Renommée pour éviter conflit avec la route HTML
data = request.get_json()
username_or_email = data.get("username_or_email")
security_answer = data.get("security_answer")
new_password = data.get("new_password")
if not username_or_email or not security_answer or not new_password:
return jsonify({"message": "Champs manquants.", "status": "Error"}), 400
success, message = reset_password_via_security_question(username_or_email, security_answer, new_password)
if success:
return jsonify({
"message": message,
"status": "Success"
}), 200
else:
return jsonify({
"message": message,
"status": "Error"
}), 400
# --- Routes de Gestion de Compte (API - Conservées) ---
@app.route("/api/user/generate-key", methods=["POST"])
@login_required
def generate_user_api_key():
user_id = session.get('user_id')
new_api_key = create_dynamic_api_key()
success, message = update_user_data(user_id, {"api_key": new_api_key})
if success:
return jsonify({
"message": "Clé API utilisateur générée et sauvegardée. Conservez-la en lieu sûr. **Utilisez-la via URL simple ('api_key=...') ou en-tête 'X-User-API-Key'.**",
"status": "Success",
"api_key": new_api_key
}), 200
else:
return jsonify({
"message": f"Erreur lors de la génération de la clé : {message}",
"status": "Error"
}), 500
@app.route("/api/user/update-info", methods=["POST"])
@login_required
def update_user_info():
user_id = session.get('user_id')
data = request.get_json()
updates = {}
if 'username' in data:
updates['username'] = data['username']
if 'email' in data:
updates['email'] = data['email']
if 'plan' in data:
updates['plan'] = data['plan']
if not updates:
return jsonify({
"message": "Aucune information à mettre à jour fournie.",
"status": "Error"
}), 400
success, message = update_user_data(user_id, updates)
if success:
return jsonify({
"message": message,
"status": "Success"
}), 200
else:
return jsonify({
"message": f"Échec de la mise à jour : {message}",
"status": "Error"
}), 400
# --- Nouvelle Route pour les Clients (API - Conservée) ---
@app.route("/api/user-register", methods=["POST"])
@user_api_key_required
def user_register_via_api_key():
client_user_id = request.user_client_id
client_data = request.user_client_data
data = request.get_json()
username = data.get("username")
email = data.get("email")
password = data.get("password")
current_count = client_data.get("created_accounts_count", 0)
plan_limit = get_plan_limit(client_data.get("plan", "free"))
if current_count >= plan_limit:
if client_data.get("plan", "free") == "free" and plan_limit == 500:
return jsonify({
"message": "Erreur: Le plan gratuit ne peut pas prendre plus de 500 comptes utilisateur.",
"status": "Error",
"code": "PLAN_LIMIT_EXCEEDED"
}), 402
return jsonify({
"message": f"Erreur: Votre plan actuel ({client_data.get('plan').upper()}) limite la création de comptes à {plan_limit}. Veuillez passer à un plan supérieur.",
"status": "Error",
"code": "PLAN_LIMIT_EXCEEDED"
}), 402
if not username or not email or len(password) < 8:
return jsonify({"message": "Nom d'utilisateur, email ou mot de passe invalide (min 8 caractères).", "status": "Error"}), 400
users = load_users_data()
if any(u.get("email") == email for u in users.values()) or any(u.get("username") == username for u in users.values()):
return jsonify({"message": "Cet email ou nom d'utilisateur est déjà enregistré.", "status": "Error"}), 400
end_user_id = str(uuid.uuid4())
hashed_password = generate_password_hash(password)
new_end_user = {
"username": username,
"email": email,
"password_hash": hashed_password,
"user_id": end_user_id,
"created_at": datetime.now().isoformat(),
"api_key": None,
"plan": "end_user",
"created_accounts_count": 0,
"security_question": None,
"security_answer": None
}
users[end_user_id] = new_end_user
new_count = current_count + 1
client_data["created_accounts_count"] = new_count
users[client_user_id] = client_data
commit_msg = f"feat: End-user registration via API Key. Client: {client_data.get('username')}. New count: {new_count}"
success_save = save_users_data(users, commit_message=commit_msg)
if not success_save:
return jsonify({"message": "Erreur critique lors de la sauvegarde (Git).", "status": "Error"}), 500
return jsonify({
"message": "Inscription de l'utilisateur final réussie.",
"status": "Success",
"user_id": end_user_id,
"accounts_remaining": plan_limit - new_count
}), 201
# ----------------------------------------------------------------------
# --- NOUVELLES ROUTES API POUR LA GESTION DES UTILISATEURS FINAUX ---
# ----------------------------------------------------------------------
@app.route("/api/enduser/register", methods=["POST"])
@api_key_required
def api_enduser_register(client_user):
"""
Route API pour l'inscription d'un utilisateur final par un client (via sa clé API).
Le 'client_user' est injecté par le décorateur api_key_required.
"""
data = request.get_json()
username = data.get("username")
email = data.get("email")
password = data.get("password")
client_user_id = client_user['user_id']
end_user_id, success, message = register_end_user(
client_user_id,
username,
email,
password
)
if success:
return jsonify({
"message": message,
"status": "Success",
"end_user_id": end_user_id,
}), 201
else:
return jsonify({"message": message, "status": "Error"}), 400
@app.route("/api/enduser/login", methods=["POST"])
@api_key_required
def api_enduser_login(client_user):
"""
Route API pour la connexion d'un utilisateur final par un client (via sa clé API).
Le 'client_user' est injecté par le décorateur api_key_required.
"""
data = request.get_json()
username_or_email = data.get("username_or_email") # Accepte username ou email
password = data.get("password")
client_user_id = client_user['user_id']
if not all([username_or_email, password]):
return jsonify({"message": "L'identifiant de l'utilisateur final et le mot de passe sont requis.", "status": "Error"}), 400
# CORRECTION : La fonction retourne (end_user_id, message, user_info)
end_user_id, message, user_info = login_end_user(
client_user_id,
username_or_email,
password
)
if end_user_id and user_info: # Si l'ID est présent (succès)
return jsonify({
"message": message,
"status": "Success",
"user": user_info
}), 200
else:
return jsonify({"message": message, "status": "Error"}), 401
@app.route("/api/enduser/recover-password", methods=["POST"])
@api_key_required
def api_enduser_recover_password(client_user):
"""
Route API pour la récupération/réinitialisation du mot de passe d'un utilisateur final
par le client (via sa clé API).
Le 'client_user' est injecté par le décorateur api_key_required.
"""
data = request.get_json()
end_user_identifier = data.get("username_or_email") # Identifiant de l'utilisateur final à réinitialiser
new_password = data.get("new_password")
client_user_id = client_user['user_id']
if not all([end_user_identifier, new_password]):
return jsonify({"message": "L'identifiant de l'utilisateur final et le nouveau mot de passe sont requis.", "status": "Error"}), 400
success, message = reset_end_user_password_by_client(
client_user_id,
end_user_identifier,
new_password
)
if success:
return jsonify({"message": message, "status": "Success"}), 200
else:
return jsonify({"message": message, "status": "Error"}), 400
# app.py
@app.route("/api/user-info", methods=["GET"])
@api_key_required
def api_user_info(client_user):
"""
Route API pour récupérer les informations de l'utilisateur principal (client)
à partir de la clé API fournie.
Le 'client_user' est injecté par le décorateur api_key_required.
"""
# client_user est l'objet utilisateur complet injecté par le décorateur
# Sécurité : créer une copie et supprimer les données sensibles avant l'envoi
user_info_safe = client_user.copy()
user_info_safe.pop('password_hash', None)
user_info_safe.pop('security_answer_hash', None)
return jsonify({
"message": "Informations utilisateur récupérées avec succès.",
"status": "Success",
"user": user_info_safe
}), 200
# --- Route de Vérification de l'État (API - Conservée) ---
@app.route("/api/health", methods=["GET"])
def health_check():
"""Vérifie l'état du service (Git)."""
try:
load_users_data() # Tente de charger les données
git_status = "Ready"
except Exception:
git_status = "Failed (Vérifier HF_TOKEN)"
return jsonify({
"status": "Online",
"data_storage": git_status
}), 200
@app.route("/", methods=["GET"])
def read_root():
"""Endpoint racine pour le Health Check (Flask version)."""
if baserow_storage.is_baserow_up():
# Statut OK (200) avec le message
return jsonify({"status": "ok", "message": "Backend and Baserow API are reachable."}), 200
else:
# Statut de service non disponible (503) avec le message d'erreur
return jsonify({"detail": "Baserow service unavailable (Health Check failed)."}), 503