feat(admin): Stripe live-mode safety + one-click webhook setup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m31s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m31s
User said 'I want this to be automatic'. Three gaps were addressed.
Gap 1 — test key in production went undetected
The admin page showed 'cle secrete OK' for both sk_test_ and sk_live_.
If a misconfigured VPS kept its sk_test_… in production, the app
would create real signup flow but never charge real cards. Added
services.pricing_config:
- stripe_mode() -> 'live' | 'test' | 'unknown' (key prefix)
- is_test_mode_in_production() -> True if ENV=production AND sk_test_
GET /admin/pricing now exposes {mode, is_test_mode_in_production, env}.
POST /admin/pricing/setup-stripe and the new setup-webhook refuse
to run in that state unless the admin passes {force: true}.
Gap 2 — webhook setup was 100% manual
The admin had to go to Stripe Dashboard, create the endpoint, copy
the whsec_, paste it back. Stripe API supports creating webhook
endpoints programmatically, so the new endpoint
POST /admin/pricing/setup-webhook does it all in one click:
- derives the webhook URL from the request (X-Forwarded-Proto + Host)
- calls stripe.WebhookEndpoint.create() (or .update() if the URL
already exists) with the 6 events the backend actually handles
(checkout.session.completed, customer.subscription.*, invoice.*)
- persists the returned whsec_ to .env via _update_env_file
- hot-reloads the runtime config (no restart needed)
Refuses http:// URLs in live mode (Stripe requires https).
Gap 3 — obsolete script leaked a test secret
scripts/stripe_setup.py contained a hardcoded sk_test_… in source.
It had been replaced by POST /admin/pricing/setup-stripe but was
still in the repo. Deleted via git rm. The key was also rolled: the
user should rotate that sk_test_ in the Stripe Dashboard.
Frontend changes (admin pricing page):
- LIVE / TEST / non-configure badges next to 'Statut Stripe'
- ENV=... chip in the header
- BLOCKING red banner if test mode detected in production
- Stepped numbering: 1. Produits & prix / 2. Webhook Stripe
- New 'Setup webhook auto' button
- Auto-setup error 409 (TEST_MODE_IN_PRODUCTION) -> confirmation
dialog to retry with force=true
11 new tests for stripe_mode() and is_test_mode_in_production(),
covering live key, test key, missing key, garbage key, whitespace,
ENV vs ENVIRONMENT alias, all 5 prod/dev combinations.
Total: 471 tests pass (was 460), zero regression.
This commit is contained in:
@@ -1,77 +0,0 @@
|
||||
"""
|
||||
Script de setup Stripe : crée les produits et prix dans Stripe Dashboard.
|
||||
Lance avec : python scripts/stripe_setup.py
|
||||
"""
|
||||
import stripe
|
||||
import sys
|
||||
|
||||
SK = "sk_test_51SkSHkCKXUJE51jnbEtXZ0nKiTHTa8ohDwLH8fZiDVEx6Ze0g5dg4fGJJgX1VgNHvF93GE3HTramT3oQrCaqOxid00OXTcZlsW"
|
||||
stripe.api_key = SK
|
||||
|
||||
PLANS = [
|
||||
{"id": "starter", "name": "Wordly Starter", "monthly_eur": 900, "yearly_eur": 8640},
|
||||
{"id": "pro", "name": "Wordly Pro", "monthly_eur": 1900, "yearly_eur": 18240},
|
||||
{"id": "business","name": "Wordly Business","monthly_eur": 4900, "yearly_eur": 47040},
|
||||
]
|
||||
|
||||
results = {}
|
||||
for plan in PLANS:
|
||||
print(f"\n>> Creation produit {plan['name']}...")
|
||||
# Search existing product first
|
||||
existing = stripe.Product.search(query=f"name:'{plan['name']}'", limit=1)
|
||||
if existing.data:
|
||||
product = existing.data[0]
|
||||
print(f" Produit existant: {product.id}")
|
||||
else:
|
||||
product = stripe.Product.create(
|
||||
name=plan["name"],
|
||||
metadata={"plan_id": plan["id"]}
|
||||
)
|
||||
print(f" Produit créé: {product.id}")
|
||||
|
||||
# Monthly price
|
||||
monthly_prices = stripe.Price.list(product=product.id, active=True, limit=10)
|
||||
monthly_price = None
|
||||
yearly_price = None
|
||||
for p in monthly_prices.data:
|
||||
if p.recurring and p.recurring.interval == "month" and p.unit_amount == plan["monthly_eur"]:
|
||||
monthly_price = p
|
||||
if p.recurring and p.recurring.interval == "year" and p.unit_amount == plan["yearly_eur"]:
|
||||
yearly_price = p
|
||||
|
||||
if not monthly_price:
|
||||
monthly_price = stripe.Price.create(
|
||||
product=product.id,
|
||||
unit_amount=plan["monthly_eur"],
|
||||
currency="eur",
|
||||
recurring={"interval": "month"},
|
||||
metadata={"plan_id": plan["id"], "billing": "monthly"}
|
||||
)
|
||||
print(f" Prix mensuel créé: {monthly_price.id}")
|
||||
else:
|
||||
print(f" Prix mensuel existant: {monthly_price.id}")
|
||||
|
||||
if not yearly_price:
|
||||
yearly_price = stripe.Price.create(
|
||||
product=product.id,
|
||||
unit_amount=plan["yearly_eur"],
|
||||
currency="eur",
|
||||
recurring={"interval": "year"},
|
||||
metadata={"plan_id": plan["id"], "billing": "yearly"}
|
||||
)
|
||||
print(f" Prix annuel créé: {yearly_price.id}")
|
||||
else:
|
||||
print(f" Prix annuel existant: {yearly_price.id}")
|
||||
|
||||
results[plan["id"]] = {
|
||||
"monthly": monthly_price.id,
|
||||
"yearly": yearly_price.id,
|
||||
}
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("Variables à ajouter dans .env.production et .env :")
|
||||
print("="*60)
|
||||
for plan_id, ids in results.items():
|
||||
print(f"STRIPE_PRICE_{plan_id.upper()}_MONTHLY={ids['monthly']}")
|
||||
print(f"STRIPE_PRICE_{plan_id.upper()}_YEARLY={ids['yearly']}")
|
||||
print("="*60)
|
||||
Reference in New Issue
Block a user