Domify commited on
Commit
6819cd2
Β·
verified Β·
1 Parent(s): 7e6550b

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +268 -3
main.py CHANGED
@@ -7,6 +7,10 @@ import hashlib
7
  import secrets
8
  import sqlite3
9
  import threading
 
 
 
 
10
  from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
11
  from pathlib import Path
12
  from datetime import datetime, timedelta
@@ -38,12 +42,24 @@ PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "https://domify-signup.hf.space")
38
  BREVO_API_KEY = os.getenv("BREVO_API_KEY", "")
39
  BREVO_FROM_EMAIL = os.getenv("BREVO_FROM_EMAIL", "domifyacademy@gmail.com")
40
  BREVO_FROM_NAME = os.getenv("BREVO_FROM_NAME", "Domify Academy")
41
- LEMON_API_KEY = os.getenv("LEMON_API_KEY", "")
42
- LEMON_WEBHOOK_SECRET = os.getenv("LEMON_WEBHOOK_SECRET", "")
43
  APPS_SCRIPT_URL = os.getenv("APPS_SCRIPT_URL", "")
44
  INTERNAL_API_TOKEN = os.getenv("INTERNAL_API_TOKEN", "")
45
  ADMIN_API_TOKEN = os.getenv("ADMIN_API_TOKEN", "")
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  ADMIN_IPS = {
48
  "fe80::f26d:78ff:fe61:be53",
49
  "192.168.7.71",
@@ -179,7 +195,14 @@ def init_db() -> None:
179
  )
180
  """
181
  )
182
-
 
 
 
 
 
 
 
183
  conn.commit()
184
  finally:
185
  conn.close()
@@ -1191,7 +1214,249 @@ async def test_email(request: Request, to_email: str):
1191
  ok = await send_brevo_email(to_email, "Test User", "Domify Test Email", html_body)
1192
  return {"success": ok}
1193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1195
  async def keep_alive_self():
1196
  while True:
1197
  try:
 
7
  import secrets
8
  import sqlite3
9
  import threading
10
+ import json
11
+ import hashlib
12
+ from datetime import datetime, timedelta
13
+ from fastapi.responses import JSONResponse
14
  from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
15
  from pathlib import Path
16
  from datetime import datetime, timedelta
 
42
  BREVO_API_KEY = os.getenv("BREVO_API_KEY", "")
43
  BREVO_FROM_EMAIL = os.getenv("BREVO_FROM_EMAIL", "domifyacademy@gmail.com")
44
  BREVO_FROM_NAME = os.getenv("BREVO_FROM_NAME", "Domify Academy")
 
 
45
  APPS_SCRIPT_URL = os.getenv("APPS_SCRIPT_URL", "")
46
  INTERNAL_API_TOKEN = os.getenv("INTERNAL_API_TOKEN", "")
47
  ADMIN_API_TOKEN = os.getenv("ADMIN_API_TOKEN", "")
48
 
49
+ # ═══════════════════════════════════════════════
50
+ # DODO PAYMENTS CONFIGURATION
51
+ # ═══════════════════════════════════════════════
52
+ DODO_PAYMENTS_API_KEY = os.getenv("DODO_PAYMENTS_API_KEY", "")
53
+ DODO_PAYMENTS_WEBHOOK_KEY = os.getenv("DODO_PAYMENTS_WEBHOOK_KEY", "")
54
+ DODO_PAYMENTS_RETURN_URL = os.getenv("DODO_PAYMENTS_RETURN_URL", "https://domify-academy.free.nf/learn-cyber-security-2.html?upgrade=success")
55
+ DODO_PAYMENTS_ENV = os.getenv("DODO_PAYMENTS_ENVIRONMENT", "test_mode")
56
+
57
+ # Product ID β†’ Tier mapping
58
+ DODO_PRODUCT_TIERS = {
59
+ "pdt_0Nf7j0m48xid1j872jfkj": {"tier": "Professional", "days": 30, "cert_paid": False},
60
+ "pdt_0Nf7lGXVl9CQ2TmHcam4T": {"tier": "Master", "days": 30, "cert_paid": True},
61
+ "pdt_0Nf7lz4x5HvTks4rzvfqN": {"tier": None, "days": 0, "cert_paid": True}, # Certificate add‑on: tier unchanged
62
+ }
63
  ADMIN_IPS = {
64
  "fe80::f26d:78ff:fe61:be53",
65
  "192.168.7.71",
 
195
  )
196
  """
197
  )
198
+ conn.execute("""
199
+ CREATE TABLE IF NOT EXISTS webhook_events (
200
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
201
+ webhook_id TEXT UNIQUE,
202
+ event_type TEXT,
203
+ processed_at TEXT
204
+ )
205
+ """)
206
  conn.commit()
207
  finally:
208
  conn.close()
 
1214
  ok = await send_brevo_email(to_email, "Test User", "Domify Test Email", html_body)
1215
  return {"success": ok}
1216
 
1217
+ # ═══════════════════════════════════════════════
1218
+ # DODO PAYMENTS – CHECKOUT SESSION
1219
+ # ═══════════════════════════════════════════════
1220
+ from pydantic import BaseModel
1221
+ from typing import Optional
1222
+
1223
+ class CheckoutRequest(BaseModel):
1224
+ product_id: str
1225
+ quantity: int = 1
1226
+ customer_email: Optional[str] = None
1227
+ customer_name: Optional[str] = None
1228
+
1229
+ @app.post("/api/create-checkout")
1230
+ async def create_checkout_session(data: CheckoutRequest):
1231
+ """
1232
+ Creates a Dodo Payments checkout session and returns the hosted checkout URL.
1233
+ The frontend redirects the user to this URL to complete payment.
1234
+ """
1235
+ if not DODO_PAYMENTS_API_KEY:
1236
+ raise HTTPException(status_code=500, detail="DODO_PAYMENTS_API_KEY not configured")
1237
+
1238
+ if data.product_id not in DODO_PRODUCT_TIERS:
1239
+ raise HTTPException(status_code=400, detail=f"Unknown product_id: {data.product_id}")
1240
+
1241
+ try:
1242
+ from dodopayments import DodoPayments
1243
+
1244
+ client = DodoPayments(
1245
+ bearer_token=DODO_PAYMENTS_API_KEY,
1246
+ environment=DODO_PAYMENTS_ENV,
1247
+ )
1248
+
1249
+ customer_block = {}
1250
+ if data.customer_email:
1251
+ customer_block["email"] = data.customer_email
1252
+ if data.customer_name:
1253
+ customer_block["name"] = data.customer_name
1254
+
1255
+ session = client.checkout_sessions.create(
1256
+ product_cart=[{
1257
+ "product_id": data.product_id,
1258
+ "quantity": data.quantity,
1259
+ }],
1260
+ customer=customer_block if customer_block else None,
1261
+ show_saved_payment_methods=True,
1262
+ feature_flags={"allow_discount_code": True},
1263
+ return_url=DODO_PAYMENTS_RETURN_URL,
1264
+ )
1265
+
1266
+ return {
1267
+ "success": True,
1268
+ "session_id": session.session_id,
1269
+ "checkout_url": session.checkout_url,
1270
+ }
1271
+
1272
+ except ImportError:
1273
+ raise HTTPException(
1274
+ status_code=500,
1275
+ detail="dodopayments SDK not installed. Run: pip install dodopayments"
1276
+ )
1277
+ except Exception as e:
1278
+ raise HTTPException(status_code=400, detail=str(e))
1279
+
1280
+
1281
+ # ═══════════════════════════════════════════════
1282
+ # DODO PAYMENTS – WEBHOOK HANDLER
1283
+ # ═══════════════════════════════════════════════
1284
+ @app.post("/webhook/dodo")
1285
+ async def dodo_webhook_handler(request: Request):
1286
+ """
1287
+ Handles Dodo Payments webhook events.
1288
+ Verifies the webhook signature using the Dodo Payments SDK and processes
1289
+ payment confirmations to upgrade user tiers.
1290
+ """
1291
+ if not DODO_PAYMENTS_WEBHOOK_KEY:
1292
+ raise HTTPException(status_code=500, detail="DODO_PAYMENTS_WEBHOOK_KEY is not set")
1293
+
1294
+ try:
1295
+ body = await request.body()
1296
+
1297
+ # ── SDK‑based signature verification (official Dodo boilerplate) ──
1298
+ try:
1299
+ from dodopayments import DodoPayments
1300
+
1301
+ client = DodoPayments(
1302
+ bearer_token=DODO_PAYMENTS_API_KEY,
1303
+ environment=DODO_PAYMENTS_ENV,
1304
+ webhook_key=DODO_PAYMENTS_WEBHOOK_KEY,
1305
+ )
1306
+ unwrapped = client.webhooks.unwrap(
1307
+ body,
1308
+ headers={
1309
+ "webhook-id": request.headers.get("webhook-id", ""),
1310
+ "webhook-signature": request.headers.get("webhook-signature", ""),
1311
+ "webhook-timestamp": request.headers.get("webhook-timestamp", ""),
1312
+ },
1313
+ )
1314
+ if not unwrapped:
1315
+ return JSONResponse(status_code=400, content={"error": "Webhook verification failed"})
1316
+
1317
+ except ImportError:
1318
+ # ── Fallback: manual HMAC‑SHA256 verification ──
1319
+ webhook_id = request.headers.get("webhook-id", "")
1320
+ webhook_signature = request.headers.get("webhook-signature", "")
1321
+ webhook_timestamp = request.headers.get("webhook-timestamp", "")
1322
+
1323
+ if not all([webhook_id, webhook_signature, webhook_timestamp]):
1324
+ return JSONResponse(status_code=400, content={"error": "Missing webhook headers"})
1325
+
1326
+ # Reject webhooks older than 5 minutes (anti‑replay)
1327
+ try:
1328
+ ts_int = int(webhook_timestamp)
1329
+ if abs(time.time() - ts_int) > 300:
1330
+ return JSONResponse(status_code=400, content={"error": "Webhook too old"})
1331
+ except ValueError:
1332
+ return JSONResponse(status_code=400, content={"error": "Invalid timestamp"})
1333
+
1334
+ signed_message = f"{webhook_id}.{webhook_timestamp}.{body.decode()}"
1335
+ expected = hmac.new(
1336
+ DODO_PAYMENTS_WEBHOOK_KEY.encode(),
1337
+ signed_message.encode(),
1338
+ hashlib.sha256,
1339
+ ).hexdigest()
1340
+
1341
+ if not hmac.compare_digest(webhook_signature, expected):
1342
+ return JSONResponse(status_code=401, content={"error": "Invalid signature"})
1343
+
1344
+ # ── Parse payload ──
1345
+ try:
1346
+ raw_text = body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else body
1347
+ payload = json.loads(raw_text)
1348
+ except json.JSONDecodeError:
1349
+ return JSONResponse(status_code=400, content={"error": "Invalid JSON"})
1350
+
1351
+ event_type = payload.get("type", "")
1352
+ event_data = payload.get("data", {})
1353
+ webhook_id_val = payload.get("id", "") or request.headers.get("webhook-id", "")
1354
 
1355
+ print(f"πŸ“¨ Dodo webhook: {event_type} (id={webhook_id_val})")
1356
+
1357
+ if event_type != "payment.succeeded":
1358
+ return {"status": "ignored", "event": event_type}
1359
+
1360
+ # ── Idempotency check ──
1361
+ conn = get_db()
1362
+ try:
1363
+ existing = conn.execute(
1364
+ "SELECT id FROM webhook_events WHERE webhook_id = ?", (webhook_id_val,)
1365
+ ).fetchone()
1366
+ if existing:
1367
+ conn.close()
1368
+ return {"status": "already_processed", "webhook_id": webhook_id_val}
1369
+
1370
+ conn.execute(
1371
+ "INSERT INTO webhook_events (webhook_id, event_type, processed_at) VALUES (?, ?, ?)",
1372
+ (webhook_id_val, event_type, datetime.utcnow().isoformat()),
1373
+ )
1374
+ conn.commit()
1375
+ finally:
1376
+ conn.close()
1377
+
1378
+ # ── Extract customer & product ──
1379
+ customer_email = event_data.get("customer", {}).get("email", "")
1380
+ # product_id may be nested in different locations
1381
+ product_id = (
1382
+ event_data.get("product_id")
1383
+ or (event_data.get("product_cart", [{}])[0].get("product_id") if event_data.get("product_cart") else None)
1384
+ or ""
1385
+ )
1386
+
1387
+ if not customer_email or not product_id:
1388
+ return {"status": "ignored", "reason": "Missing email or product_id"}
1389
+
1390
+ product_config = DODO_PRODUCT_TIERS.get(product_id)
1391
+ if not product_config:
1392
+ return {"status": "ignored", "reason": f"Unknown product_id: {product_id}"}
1393
+
1394
+ # ── Update user tier ──
1395
+ conn = get_db()
1396
+ try:
1397
+ with DB_LOCK:
1398
+ user = conn.execute(
1399
+ "SELECT * FROM users WHERE email = ?", (customer_email,)
1400
+ ).fetchone()
1401
+
1402
+ if user:
1403
+ if product_config["tier"] is not None:
1404
+ # Normal tier upgrade
1405
+ expiry = None
1406
+ if product_config["days"] > 0:
1407
+ expiry = (datetime.utcnow() + timedelta(days=product_config["days"])).isoformat()
1408
+ conn.execute(
1409
+ "UPDATE users SET tier = ?, expiry = ?, cert_paid = ? WHERE email = ?",
1410
+ (product_config["tier"], expiry, 1 if product_config["cert_paid"] else 0, customer_email),
1411
+ )
1412
+ else:
1413
+ # Certificate add‑on: only flip cert_paid, leave tier/expiry alone
1414
+ conn.execute(
1415
+ "UPDATE users SET cert_paid = 1 WHERE email = ?",
1416
+ (customer_email,),
1417
+ )
1418
+ print(f"βœ… Upgraded {customer_email} to {product_config['tier'] or 'certificate add-on'}")
1419
+ else:
1420
+ # Create user if they don't exist yet
1421
+ expiry = None
1422
+ if product_config["days"] > 0:
1423
+ expiry = (datetime.utcnow() + timedelta(days=product_config["days"])).isoformat()
1424
+ tier = product_config["tier"] or "Free"
1425
+ conn.execute(
1426
+ """
1427
+ INSERT INTO users (user_id, full_name, email, signup_date, country, source,
1428
+ ip, timestamp, tier, expiry, cert_paid, cert_generated)
1429
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1430
+ """,
1431
+ (
1432
+ f"dodo_{webhook_id_val}",
1433
+ customer_email,
1434
+ customer_email,
1435
+ datetime.utcnow().isoformat(),
1436
+ "",
1437
+ "dodo_payment",
1438
+ "",
1439
+ datetime.utcnow().isoformat(),
1440
+ tier,
1441
+ expiry,
1442
+ 1 if product_config["cert_paid"] else 0,
1443
+ 0,
1444
+ ),
1445
+ )
1446
+ print(f"βœ… Created new user {customer_email} via Dodo payment")
1447
+
1448
+ conn.commit()
1449
+ finally:
1450
+ conn.close()
1451
+
1452
+ return {"status": "success", "tier": product_config.get("tier")}
1453
+
1454
+ except HTTPException:
1455
+ raise
1456
+ except Exception as e:
1457
+ print(f"❌ Dodo webhook error: {str(e)}")
1458
+ return JSONResponse(status_code=500, content={"error": str(e)})
1459
+
1460
  async def keep_alive_self():
1461
  while True:
1462
  try: