Spaces:
Running
Running
| # 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): | |
| 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').""" | |
| 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) --- | |
| 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 | |
| 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 | |
| def logout(): | |
| session.pop('user_id', None) | |
| return jsonify({"message": "Déconnexion réussie.", "status": "Success"}), 200 | |
| 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) --- | |
| 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 | |
| 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) --- | |
| 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 --- | |
| # ---------------------------------------------------------------------- | |
| 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 | |
| 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 | |
| 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 | |
| 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) --- | |
| 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 | |
| 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 |