From d39c7d3319bca75bf5e4e06d0a74e047ab5caf97 Mon Sep 17 00:00:00 2001 From: CTO H3R7Tech Date: Mon, 27 Apr 2026 15:21:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(billing):=20JWT=20token=20incompatibility?= =?UTF-8?q?=20=E2=80=94=20use=20saas=5Fauth=20require=5Fauth=20+=20fix=20t?= =?UTF-8?q?able=20names=20HRT-54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Paperclip --- api_v1/routes/billing.py | 38 +++++++++++++++++++------------------- billing_db.py | 24 ++++++++++++++++++++---- multi_scraper_v5.py | 2 +- pmu_results.py | 2 +- saas_api_v1.py | 27 ++++++++++++++++++++++++++- scoring_v2.py | 2 +- 6 files changed, 68 insertions(+), 27 deletions(-) diff --git a/api_v1/routes/billing.py b/api_v1/routes/billing.py index 35d8fdc..085dc6d 100644 --- a/api_v1/routes/billing.py +++ b/api_v1/routes/billing.py @@ -24,9 +24,9 @@ import os from datetime import datetime, timedelta, timezone import stripe -from flask import Blueprint, g, jsonify, request +from flask import Blueprint, jsonify, request -from auth import jwt_required_middleware +from saas_auth import require_auth as jwt_required_middleware from billing_db import get_db, migrate_billing_tables logger = logging.getLogger("turf_saas.billing") @@ -73,18 +73,18 @@ def _sget(obj, key, default=None): return default -def _get_active_subscription(db, user_id: int): +def _get_active_subscription(db, user_id): """Return the most recent active subscription row for a user.""" return db.execute( - """SELECT * FROM subscriptions + """SELECT * FROM saas_subscriptions WHERE user_id = ? ORDER BY start_date DESC LIMIT 1""", - (user_id,), + (str(user_id),), ).fetchone() -def _upsert_subscription(db, user_id: int, **fields): +def _upsert_subscription(db, user_id, **fields): """ Update existing subscription or insert a new one. fields: plan, stripe_customer_id, stripe_subscription_id, @@ -95,19 +95,19 @@ def _upsert_subscription(db, user_id: int, **fields): # Build SET clause dynamically from provided fields set_parts = ", ".join(f"{k} = ?" for k in fields) values = list(fields.values()) + [existing["id"]] - db.execute(f"UPDATE subscriptions SET {set_parts} WHERE id = ?", values) + db.execute(f"UPDATE saas_subscriptions SET {set_parts} WHERE id = ?", values) else: cols = ", ".join(["user_id"] + list(fields.keys())) placeholders = ", ".join(["?"] * (1 + len(fields))) - values = [user_id] + list(fields.values()) + values = [str(user_id)] + list(fields.values()) db.execute( - f"INSERT INTO subscriptions ({cols}) VALUES ({placeholders})", values + f"INSERT INTO saas_subscriptions ({cols}) VALUES ({placeholders})", values ) -def _update_user_plan(db, user_id: int, plan: str): - """Sync users.plan field to match active subscription.""" - db.execute("UPDATE users SET plan = ? WHERE id = ?", (plan, user_id)) +def _update_user_plan(db, user_id, plan: str): + """Sync saas_users.plan field to match active subscription.""" + db.execute("UPDATE saas_users SET plan = ? WHERE id = ?", (plan, str(user_id))) def _get_or_create_stripe_customer(user, db) -> str: @@ -198,7 +198,7 @@ def create_checkout(): if not price_id: return jsonify({"error": f"Prix Stripe non configuré pour le plan {plan}"}), 503 - user = g.current_user + user = request.current_user if user["plan"] == plan: return jsonify({"error": f"Vous êtes déjà sur le plan {plan}"}), 400 @@ -263,7 +263,7 @@ def create_portal(): if not stripe.api_key: return jsonify({"error": "Stripe non configuré"}), 503 - user = g.current_user + user = request.current_user db = get_db() try: sub = _get_active_subscription(db, user["id"]) @@ -309,7 +309,7 @@ def billing_status(): 200: description: Subscription status """ - user = g.current_user + user = request.current_user db = get_db() try: sub = _get_active_subscription(db, user["id"]) @@ -428,7 +428,7 @@ def stripe_webhook(): def _resolve_user_from_customer(db, customer_id: str): """Look up user_id via subscriptions.stripe_customer_id.""" row = db.execute( - "SELECT user_id FROM subscriptions WHERE stripe_customer_id = ? LIMIT 1", + "SELECT user_id FROM saas_subscriptions WHERE stripe_customer_id = ? LIMIT 1", (customer_id,), ).fetchone() if row: @@ -465,7 +465,7 @@ def _handle_checkout_completed(db, event): user_id = _sget(metadata, "user_id") if user_id: - user_id = int(user_id) + user_id = str(user_id) else: user_id = _resolve_user_from_customer(db, customer_id) @@ -531,7 +531,7 @@ def _handle_subscription_updated(db, event): meta = _sget(sub_obj, "metadata") or {} meta_uid = _sget(meta, "user_id") if meta_uid: - user_id = int(meta_uid) + user_id = str(meta_uid) if not user_id: logger.error( @@ -565,7 +565,7 @@ def _handle_subscription_deleted(db, event): meta = _sget(sub_obj, "metadata") or {} meta_uid = _sget(meta, "user_id") if meta_uid: - user_id = int(meta_uid) + user_id = str(meta_uid) if not user_id: logger.error( diff --git a/billing_db.py b/billing_db.py index 93d5096..2a2c1bd 100644 --- a/billing_db.py +++ b/billing_db.py @@ -76,14 +76,30 @@ def migrate_billing_tables(): id INTEGER PRIMARY KEY AUTOINCREMENT, stripe_event_id TEXT NOT NULL UNIQUE, event_type TEXT NOT NULL, - user_id INTEGER REFERENCES users(id), + user_id TEXT, payload TEXT, processed_at DATETIME NOT NULL DEFAULT (datetime('now')) ); - CREATE INDEX IF NOT EXISTS idx_billing_events_user ON billing_events(user_id); - CREATE INDEX IF NOT EXISTS idx_billing_events_type ON billing_events(event_type); - CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe ON subscriptions(stripe_subscription_id); + CREATE TABLE IF NOT EXISTS saas_subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + plan TEXT NOT NULL DEFAULT 'free', + start_date DATETIME DEFAULT (datetime('now')), + end_date DATETIME, + stripe_customer_id TEXT, + stripe_subscription_id TEXT, + status TEXT NOT NULL DEFAULT 'active', + grace_period_end DATETIME, + current_period_end DATETIME + ); + + CREATE INDEX IF NOT EXISTS idx_billing_events_user ON billing_events(user_id); + CREATE INDEX IF NOT EXISTS idx_billing_events_type ON billing_events(event_type); + CREATE INDEX IF NOT EXISTS idx_saas_subs_user ON saas_subscriptions(user_id); + CREATE INDEX IF NOT EXISTS idx_saas_subs_customer ON saas_subscriptions(stripe_customer_id); + CREATE INDEX IF NOT EXISTS idx_saas_subs_stripe ON saas_subscriptions(stripe_subscription_id); + CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe ON subscriptions(stripe_subscription_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON subscriptions(stripe_customer_id); """) diff --git a/multi_scraper_v5.py b/multi_scraper_v5.py index 3f2c515..cdae170 100755 --- a/multi_scraper_v5.py +++ b/multi_scraper_v5.py @@ -15,7 +15,7 @@ import sqlite3 import re import os -DB_PATH = "/home/h3r7/turf_scraper/turf.db" +DB_PATH = "/home/h3r7/turf_saas/turf_saas.db" HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8', diff --git a/pmu_results.py b/pmu_results.py index 2f9713d..f2cd827 100755 --- a/pmu_results.py +++ b/pmu_results.py @@ -38,7 +38,7 @@ from pathlib import Path # ───────────────────────────────────────────────────────── # CONFIG # ───────────────────────────────────────────────────────── -DB_PATH = "/home/h3r7/turf_scraper/turf.db" +DB_PATH = "/home/h3r7/turf_saas/turf_saas.db" OUTPUT_DIR = Path("/home/h3r7/turf_scraper") API_BASE = "https://online.turfinfo.api.pmu.fr/rest/client/7" diff --git a/saas_api_v1.py b/saas_api_v1.py index 3bb020b..186ad89 100644 --- a/saas_api_v1.py +++ b/saas_api_v1.py @@ -9,7 +9,7 @@ from flask import Blueprint, request, jsonify import sqlite3 import os from datetime import datetime -from .saas_auth import require_auth +from saas_auth import require_auth DB_PATH = os.environ.get("TURF_SAAS_DB", "/home/h3r7/turf_saas/turf_saas.db") @@ -255,3 +255,28 @@ def export_csv(): "Content-Disposition": f"attachment; filename=turf_ia_{date_param}.csv" }, ) + + +# ─── Billing Blueprint (Stripe) + JWT init — HRT-49 ───────────────────────── +# Registers /api/v1/billing/* routes via nested Blueprint (Flask 2.0+) +# Also initializes JWTManager on the Flask app (required for jwt_required_middleware) +try: + from flask_jwt_extended import JWTManager + from api_v1.routes.billing import billing_bp + + # Initialize JWTManager on the Flask app when api_v1_bp is registered + @api_v1_bp.record_once + def _init_jwt(state): + app = state.app + if not app.config.get('JWT_SECRET_KEY'): + import os + app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY', 'turf-saas-secret-key-change-in-prod') + if 'flask_jwt_extended' not in app.extensions: + JWTManager(app) + + # Register billing blueprint with url_prefix='/billing' + # (parent api_v1_bp has '/api/v1', so result is /api/v1/billing/*) + api_v1_bp.register_blueprint(billing_bp, url_prefix='/billing') + print('[saas_api_v1] Billing blueprint (Stripe) + JWT registered ✅') +except Exception as _billing_err: + print(f'[saas_api_v1] Warning: billing blueprint not loaded: {_billing_err}') diff --git a/scoring_v2.py b/scoring_v2.py index 48dec94..0fd72f6 100755 --- a/scoring_v2.py +++ b/scoring_v2.py @@ -10,7 +10,7 @@ import json import re from datetime import datetime -DB_PATH = "/home/h3r7/turf_scraper/turf.db" +DB_PATH = "/home/h3r7/turf_saas/turf_saas.db" HEADERS = {'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json'} def get_cote_from_db(horse_name, date_course):