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:
@@ -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(
|
||||||
|
|||||||
@@ -76,14 +76,30 @@ 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 INDEX IF NOT EXISTS idx_billing_events_user ON billing_events(user_id);
|
CREATE TABLE IF NOT EXISTS saas_subscriptions (
|
||||||
CREATE INDEX IF NOT EXISTS idx_billing_events_type ON billing_events(event_type);
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe ON subscriptions(stripe_subscription_id);
|
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);
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON subscriptions(stripe_customer_id);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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}')
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user