# 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