Update main.py
Browse files
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:
|