#!/usr/bin/env python3 """ Turf SaaS API v1 — Auth JWT + Multi-tenant Sprint 2-3: HRT-28 Run: FLASK_ENV=development ./venv/bin/python saas_api.py Ports (isolated from production): Portal: 8793 SaaS API: 8792 ← this file Dashboard: 8791 Combined API: 8790 """ import os import logging import logging.handlers import sys from flask import Flask, jsonify, g, request from flask_cors import CORS from flask_jwt_extended import JWTManager, get_jwt from auth_db import init_auth_tables from auth import ( auth_bp, jwt_required_middleware, plan_required, free_daily_limit_check, _get_user_by_id, ) from middleware import rate_limit_middleware, access_log_middleware # ────────────────────────────────────────────────────────────── # Logging setup # ────────────────────────────────────────────────────────────── LOG_DIR = os.path.join(os.path.dirname(__file__), "logs") os.makedirs(LOG_DIR, exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[ logging.StreamHandler(sys.stdout), logging.handlers.RotatingFileHandler( os.path.join(LOG_DIR, "saas_api.log"), maxBytes=5 * 1024 * 1024, backupCount=3, ), ], ) # ────────────────────────────────────────────────────────────── # App factory # ────────────────────────────────────────────────────────────── def create_app(test_config=None): app = Flask(__name__) # JWT config app.config["JWT_SECRET_KEY"] = os.environ.get( "JWT_SECRET_KEY", "CHANGE_ME_IN_PRODUCTION_" + os.urandom(24).hex() ) app.config["JWT_ACCESS_TOKEN_EXPIRES"] = 900 # 15 minutes app.config["JWT_REFRESH_TOKEN_EXPIRES"] = 2592000 # 30 days if test_config: app.config.update(test_config) # CORS — SaaS domain + localhost for dev CORS( app, origins=os.environ.get( "CORS_ORIGINS", "http://localhost:8793,http://127.0.0.1:8793,https://turf-ia.h3r7.tech", ).split(","), methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "Authorization"], supports_credentials=True, ) # JWT jwt = JWTManager(app) # ── JWT error handlers ──────────────────────────────────── @jwt.expired_token_loader def expired_token(_jwt_header, _jwt_payload): return jsonify({"error": "Token expiré"}), 401 @jwt.invalid_token_loader def invalid_token(reason): return jsonify({"error": "Token invalide", "detail": reason}), 422 @jwt.unauthorized_loader def unauthorized(reason): return jsonify({"error": "Token manquant ou invalide", "detail": reason}), 401 # ── Register middleware ─────────────────────────────────── rate_limit_middleware(app) access_log_middleware(app) # ── Blueprints ──────────────────────────────────────────── app.register_blueprint(auth_bp) # ── Predictions routes (multi-tenant plan check) ────────── @app.route("/api/v1/predictions", methods=["GET"]) @jwt_required_middleware @free_daily_limit_check def predictions(): """ GET /api/v1/predictions - free: Top 3 uniquement (déjà filtrées par le moteur ML) - premium: toutes courses + alertes Telegram - pro: API complète + export CSV disponible """ user = g.current_user plan = user["plan"] # Forward to combined_api for actual predictions import requests as req try: params = dict(request.args) resp = req.get( "http://localhost:8790/api/predictions", params=params, timeout=10, ) data = resp.json() except Exception as e: return jsonify( {"error": "Service prédictions indisponible", "detail": str(e)} ), 503 # Plan filtering if plan == "free": # Top 3 only if isinstance(data, list): data = [ {k: v for k, v in p.items() if k not in ("score_detaille",)} for p in data[:3] ] return jsonify({"plan": plan, "predictions": data, "limit": "Top 3"}), 200 elif plan == "premium": # All courses, but no CSV export return jsonify( {"plan": plan, "predictions": data, "telegram_alerts": True} ), 200 else: # pro return jsonify( { "plan": plan, "predictions": data, "telegram_alerts": True, "csv_export_url": "/api/v1/predictions/export", } ), 200 @app.route("/api/v1/predictions/export", methods=["GET"]) @jwt_required_middleware @plan_required("pro") def predictions_export(): """CSV export — pro plan only.""" import requests as req import io try: resp = req.get( "http://localhost:8790/api/predictions/export", params=dict(request.args), timeout=15, ) from flask import Response return Response( resp.content, mimetype="text/csv", headers={"Content-Disposition": "attachment; filename=predictions.csv"}, ) except Exception as e: return jsonify({"error": "Export indisponible", "detail": str(e)}), 503 @app.route("/api/v1/subscription/upgrade", methods=["GET"]) @jwt_required_middleware def subscription_info(): """Return available plans and current user plan.""" user = g.current_user return jsonify( { "current_plan": user["plan"], "plans": { "free": { "price": "0€/mois", "features": ["Top 3 prédictions", "1 course/jour"], }, "premium": { "price": "9.99€/mois", "features": [ "Toutes les courses", "Alertes Telegram", "Historique 30j", ], }, "pro": { "price": "29.99€/mois", "features": [ "API complète", "Export CSV", "Alertes Telegram", "Historique illimité", "Support prioritaire", ], }, }, "upgrade_contact": "contact@h3r7.tech", } ), 200 # ── Health check ────────────────────────────────────────── @app.route("/api/v1/health", methods=["GET"]) def health(): return jsonify( {"status": "ok", "service": "turf-saas-api", "version": "2.3.0"} ), 200 # Init DB tables on startup with app.app_context(): init_auth_tables() return app # ────────────────────────────────────────────────────────────── # Entrypoint # ────────────────────────────────────────────────────────────── if __name__ == "__main__": app = create_app() port = int(os.environ.get("SAAS_API_PORT", 8792)) app.run(host="0.0.0.0", port=port, debug=False)