fix(billing): JWT token incompatibility — use saas_auth require_auth + fix table names HRT-54

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO H3R7Tech
2026-04-27 15:21:43 +02:00
parent 8c5fdf1e9c
commit d39c7d3319
6 changed files with 68 additions and 27 deletions

View File

@@ -24,9 +24,9 @@ import os
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import stripe 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 from billing_db import get_db, migrate_billing_tables
logger = logging.getLogger("turf_saas.billing") logger = logging.getLogger("turf_saas.billing")
@@ -73,18 +73,18 @@ def _sget(obj, key, default=None):
return default 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 the most recent active subscription row for a user."""
return db.execute( return db.execute(
"""SELECT * FROM subscriptions """SELECT * FROM saas_subscriptions
WHERE user_id = ? WHERE user_id = ?
ORDER BY start_date DESC ORDER BY start_date DESC
LIMIT 1""", LIMIT 1""",
(user_id,), (str(user_id),),
).fetchone() ).fetchone()
def _upsert_subscription(db, user_id: int, **fields): def _upsert_subscription(db, user_id, **fields):
""" """
Update existing subscription or insert a new one. Update existing subscription or insert a new one.
fields: plan, stripe_customer_id, stripe_subscription_id, 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 # Build SET clause dynamically from provided fields
set_parts = ", ".join(f"{k} = ?" for k in fields) set_parts = ", ".join(f"{k} = ?" for k in fields)
values = list(fields.values()) + [existing["id"]] 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: else:
cols = ", ".join(["user_id"] + list(fields.keys())) cols = ", ".join(["user_id"] + list(fields.keys()))
placeholders = ", ".join(["?"] * (1 + len(fields))) placeholders = ", ".join(["?"] * (1 + len(fields)))
values = [user_id] + list(fields.values()) values = [str(user_id)] + list(fields.values())
db.execute( 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): def _update_user_plan(db, user_id, plan: str):
"""Sync users.plan field to match active subscription.""" """Sync saas_users.plan field to match active subscription."""
db.execute("UPDATE users SET plan = ? WHERE id = ?", (plan, user_id)) db.execute("UPDATE saas_users SET plan = ? WHERE id = ?", (plan, str(user_id)))
def _get_or_create_stripe_customer(user, db) -> str: def _get_or_create_stripe_customer(user, db) -> str:
@@ -198,7 +198,7 @@ def create_checkout():
if not price_id: if not price_id:
return jsonify({"error": f"Prix Stripe non configuré pour le plan {plan}"}), 503 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: if user["plan"] == plan:
return jsonify({"error": f"Vous êtes déjà sur le plan {plan}"}), 400 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: if not stripe.api_key:
return jsonify({"error": "Stripe non configuré"}), 503 return jsonify({"error": "Stripe non configuré"}), 503
user = g.current_user user = request.current_user
db = get_db() db = get_db()
try: try:
sub = _get_active_subscription(db, user["id"]) sub = _get_active_subscription(db, user["id"])
@@ -309,7 +309,7 @@ def billing_status():
200: 200:
description: Subscription status description: Subscription status
""" """
user = g.current_user user = request.current_user
db = get_db() db = get_db()
try: try:
sub = _get_active_subscription(db, user["id"]) sub = _get_active_subscription(db, user["id"])
@@ -428,7 +428,7 @@ def stripe_webhook():
def _resolve_user_from_customer(db, customer_id: str): def _resolve_user_from_customer(db, customer_id: str):
"""Look up user_id via subscriptions.stripe_customer_id.""" """Look up user_id via subscriptions.stripe_customer_id."""
row = db.execute( 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,), (customer_id,),
).fetchone() ).fetchone()
if row: if row:
@@ -465,7 +465,7 @@ def _handle_checkout_completed(db, event):
user_id = _sget(metadata, "user_id") user_id = _sget(metadata, "user_id")
if user_id: if user_id:
user_id = int(user_id) user_id = str(user_id)
else: else:
user_id = _resolve_user_from_customer(db, customer_id) 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 = _sget(sub_obj, "metadata") or {}
meta_uid = _sget(meta, "user_id") meta_uid = _sget(meta, "user_id")
if meta_uid: if meta_uid:
user_id = int(meta_uid) user_id = str(meta_uid)
if not user_id: if not user_id:
logger.error( logger.error(
@@ -565,7 +565,7 @@ def _handle_subscription_deleted(db, event):
meta = _sget(sub_obj, "metadata") or {} meta = _sget(sub_obj, "metadata") or {}
meta_uid = _sget(meta, "user_id") meta_uid = _sget(meta, "user_id")
if meta_uid: if meta_uid:
user_id = int(meta_uid) user_id = str(meta_uid)
if not user_id: if not user_id:
logger.error( logger.error(

View File

@@ -76,13 +76,29 @@ def migrate_billing_tables():
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
stripe_event_id TEXT NOT NULL UNIQUE, stripe_event_id TEXT NOT NULL UNIQUE,
event_type TEXT NOT NULL, event_type TEXT NOT NULL,
user_id INTEGER REFERENCES users(id), user_id TEXT,
payload TEXT, payload TEXT,
processed_at DATETIME NOT NULL DEFAULT (datetime('now')) processed_at DATETIME NOT NULL DEFAULT (datetime('now'))
); );
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_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_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_stripe ON subscriptions(stripe_subscription_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON subscriptions(stripe_customer_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON subscriptions(stripe_customer_id);
""") """)

View File

@@ -15,7 +15,7 @@ import sqlite3
import re import re
import os import os
DB_PATH = "/home/h3r7/turf_scraper/turf.db" DB_PATH = "/home/h3r7/turf_saas/turf_saas.db"
HEADERS = { 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', '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', 'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',

View File

@@ -38,7 +38,7 @@ from pathlib import Path
# ───────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────
# CONFIG # 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") OUTPUT_DIR = Path("/home/h3r7/turf_scraper")
API_BASE = "https://online.turfinfo.api.pmu.fr/rest/client/7" API_BASE = "https://online.turfinfo.api.pmu.fr/rest/client/7"

View File

@@ -9,7 +9,7 @@ from flask import Blueprint, request, jsonify
import sqlite3 import sqlite3
import os import os
from datetime import datetime 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") 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" "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}')

View File

@@ -10,7 +10,7 @@ import json
import re import re
from datetime import datetime 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'} HEADERS = {'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json'}
def get_cote_from_db(horse_name, date_course): def get_cote_from_db(horse_name, date_course):