# main.py - Domify Academy Backend with Lemon Squeezy from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware import sqlite3 from datetime import datetime, timedelta import asyncio import aiohttp import time import hmac import hashlib import sqlite3 import requests from datetime import datetime from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware app = FastAPI() app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) # ============================================ # CORS - allow your frontend domains # ============================================ app.add_middleware( CORSMiddleware, allow_origins=[ "https://domify-academy.free.nf", "https://*.free.nf", "http://localhost:3000" ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ============================================ # DATABASE SETUP (SQLite) # ============================================ DB_PATH = "domify_users.db" conn = sqlite3.connect(DB_PATH, check_same_thread=False) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS users ( user_id TEXT PRIMARY KEY, full_name TEXT, email TEXT UNIQUE, signup_date TEXT, country TEXT, source TEXT, ip TEXT, timestamp TEXT, tier TEXT DEFAULT 'Free', expiry TEXT, cert_paid INTEGER DEFAULT 0, cert_generated INTEGER DEFAULT 0 ) """) conn.commit() # ============================================ # ADMIN IPS (add your own IP addresses here) # ============================================ ADMIN_IPS = ["fe80::f26d:78ff:fe61:be53", "192.168.7.71", "10.92.98.240"] # ============================================ # LEMON SQUEEZY CONFIGURATION # ============================================ LEMON_STORE_ID = 342813 LEMON_API_KEY = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiI5NGQ1OWNlZi1kYmI4LTRlYTUtYjE3OC1kMjU0MGZjZDY5MTkiLCJqdGkiOiIyM2Q5MTk2NGQ2ZDk4OTc5ZGUyY2NmNGViYTNkZTA5ODMyZWFiNGIwZDk5MmI3MWVkY2E3NDExODU4ZmMzODA4NjVlMWZhNzlkNTUyNTY2NiIsImlhdCI6MTc3NTk2MzY4NS4zMjk5NjksIm5iZiI6MTc3NTk2MzY4NS4zMjk5NzIsImV4cCI6NDk0OTA3ODQwMC4wMzk4NjQsInN1YiI6IjY4Nzc3MzkiLCJzY29wZXMiOltdfQ.RqMkdIptrE52m_Dr_sI674lUae-FksX-Ey4metBS6QY4r33yyoYvD5DVOCLTa4q6dnKo2b5hhWl_hONYnNyjLwkFemsWOkyy6c17jWn4IC1S59co-iHaz5vYa3PTttWF9nSHQpoyRFhd9dNxGfBH7z4x06PoqDGUVEd3Am60xwXAoe5oXILrpigdC81Z0sk9pnw4gJXrzKlP88bKfjGmqHawowpXwvqNoiKjPSZCcYLQKWy2GJ6yPdO-z3SqWKP8RJ64lp3nHDLcfJGmJLlRMTLCubLdRvX_LXi6EE5bzMWBdOvbszL9h00NZE9DL_7Qd6tufh_uYgqg3b8LQK-Ur-i6E60DSAktke3wwI46MCXyl8zl-lMeuGYUq98TIV8eTGvSKroNYs-dwI068iFePI8qgt1YeF4MAQendj1vJlIVQqB3C79D7O0OlVLAkibJ50yCAf6B63q9F1leVOpfhXlJWZvZuLMkQy4vOGu7DYUPRRP0SRq3g_e_ozFgzbzb51e-hUUiU4Q_1lEWlsFuaSoXen0iJ4K2llrKmfR0FTKep2JYoRWQ473IvrL7Uxh_K87Z9SQzDUSHDL4b4tGqSwWrElT1kDFxHCDeKFjloYFHkfoAHgpeqstTj_PgdOpwaVvgTWDIj6Vw5sREEvamheEb6e9pRyg90fJlmLGdygo" LEMON_WEBHOOK_SECRET = "whsec_8f3a9d2c5e1b7a4d6f8e2c9b5a7d3f1e" # Map variant IDs to tiers LEMON_PRODUCTS = { 1519026: {"tier": "Professional", "days": 30}, 1519043: {"tier": "Master", "days": 30}, 1519056: {"tier": "Certificate", "days": 0}, } # ============================================ # HELPER: get real client IP # ============================================ def get_client_ip(request: Request) -> str: forwarded = request.headers.get("X-Forwarded-For") if forwarded: return forwarded.split(",")[0].strip() return request.client.host # ============================================ # ENDPOINT 1: SIGNUP # ============================================ @app.post("/signup") async def signup(request: Request): try: data = await request.json() required = ['userId', 'fullName', 'email', 'signupDate', 'country', 'source', 'ip', 'timestamp'] for field in required: if field not in data: raise HTTPException(status_code=400, detail=f"Missing field: {field}") c.execute(""" INSERT OR REPLACE INTO users (user_id, full_name, email, signup_date, country, source, ip, timestamp, tier, expiry, cert_paid, cert_generated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( data['userId'], data['fullName'], data['email'], data['signupDate'], data['country'], data['source'], data['ip'], data['timestamp'], data.get('tier', 'Free'), data.get('expiry'), int(data.get('certPaid', False)), int(data.get('certGenerated', False)) )) conn.commit() return {"status": "ok", "message": "User saved"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ============================================ # ENDPOINT 2: GET USER DATA # ============================================ @app.get("/user") async def get_user(request: Request, email: str = None, user_id: str = None): client_ip = get_client_ip(request) # Admin override if client_ip in ADMIN_IPS: return { "userId": "admin", "name": "Administrator", "email": "domifyacademy@gmail.com", "tier": "Master", "expiry": (datetime.now() + timedelta(days=365)).isoformat(), "certPaid": True, "certGenerated": False, "isAdmin": True } # Find user c.execute(""" SELECT user_id, full_name, email, tier, expiry, cert_paid, cert_generated FROM users WHERE email=? OR user_id=? OR ip=? """, (email, user_id, client_ip)) row = c.fetchone() if row: return { "userId": row[0], "name": row[1], "email": row[2], "tier": row[3], "expiry": row[4], "certPaid": bool(row[5]), "certGenerated": bool(row[6]), "isAdmin": False } raise HTTPException(status_code=404, detail="User not found") # ============================================ # ENDPOINT 3: LEMON SQUEEZY WEBHOOK # ============================================ @app.post("/webhook/lemonsqueezy") async def lemon_webhook(request: Request): try: # Get raw body and signature body = await request.body() signature = request.headers.get("X-Signature") if not signature: print("❌ No signature in webhook") return {"status": "ignored", "reason": "no signature"} # Verify signature expected = hmac.new( LEMON_WEBHOOK_SECRET.encode(), body, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(signature, expected): print("❌ Invalid signature") return {"status": "ignored", "reason": "invalid signature"} # Parse data data = await request.json() event_name = data.get("meta", {}).get("event_name") if event_name != "order_created": return {"status": "ignored", "reason": f"event {event_name}"} # Extract customer email and variant order_data = data.get("data", {}).get("attributes", {}) email = order_data.get("user_email") first_item = order_data.get("first_order_item", {}) variant_id = first_item.get("variant_id") if not email or not variant_id: print(f"❌ Missing email or variant") return {"status": "ignored", "reason": "missing email or variant"} # Map variant to product product = LEMON_PRODUCTS.get(variant_id) if not product: print(f"❌ Unknown variant: {variant_id}") return {"status": "ignored", "reason": f"unknown variant {variant_id}"} print(f"✅ Processing payment: {email} -> {product['tier']}") # Calculate expiry expiry = None if product["days"] > 0: expiry = (datetime.now() + timedelta(days=product["days"])).isoformat() # Update database c.execute("SELECT * FROM users WHERE email = ?", (email,)) user = c.fetchone() if user: c.execute("UPDATE users SET tier = ?, expiry = ? WHERE email = ?", (product["tier"], expiry, email)) if product["tier"] == "Master": c.execute("UPDATE users SET cert_paid = 1 WHERE email = ?", (email,)) print(f"✅ Updated existing user: {email}") else: c.execute(""" INSERT INTO users (user_id, full_name, email, tier, expiry, cert_paid, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?) """, ( f"auto_{int(time.time())}", email, email, product["tier"], expiry, 1 if product["tier"] == "Master" else 0, datetime.now().isoformat() )) print(f"✅ Created new user: {email}") conn.commit() return {"status": "ok", "message": f"Updated {email} to {product['tier']}"} except Exception as e: print(f"❌ Webhook error: {e}") return {"status": "error", "reason": str(e)} # ============================================ # HEALTH CHECK # ============================================ @app.get("/") def health(): return {"status": "alive", "service": "Domify Academy Backend", "version": "3.0"} # ============================================ # KEEP-ALIVE FUNCTION (Prevents HF Space from sleeping) # ============================================ async def keep_alive_self(): url = "https://domify-signup.hf.space" while True: try: async with aiohttp.ClientSession() as session: async with session.get(url) as response: print(f"✅ Self-ping at {time.ctime()}: {response.status}") except Exception as e: print(f"❌ Self-ping failed: {e}") await asyncio.sleep(300) @app.on_event("startup") async def startup_event(): print("🚀 Domify Academy Backend Starting...") asyncio.create_task(keep_alive_self()) print("✅ Keep-alive task started (pings every 5 minutes)") # Google Apps Script Webhook URL (deploy this first) APPS_SCRIPT_URL = "https://script.google.com/macros/s/AKfycbwRuM5jLusScp0gzZqOmjO3_V_PXvn72vG4GSZ9P5jNb9kwav6_GVf5g0zWI7CwrXGb/exec" # ============================================ # STORE CERTIFICATE CODE (First call from certificate page) # ============================================ @app.post("/store-certificate-code") async def store_certificate_code(request: Request): try: data = await request.json() verification_code = data.get('verification_code') user_name = data.get('user_name') user_email = data.get('user_email') user_id = data.get('user_id') tier = data.get('tier') date = data.get('date') # Save to SQLite (without Drive URL yet) conn = sqlite3.connect('domify_users.db') c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS certificates ( id INTEGER PRIMARY KEY AUTOINCREMENT, verification_code TEXT UNIQUE, user_name TEXT, user_email TEXT, user_id TEXT, tier TEXT, date TEXT, drive_url TEXT ) """) c.execute(""" INSERT OR REPLACE INTO certificates (verification_code, user_name, user_email, user_id, tier, date, drive_url) VALUES (?, ?, ?, ?, ?, ?, ?) """, (verification_code, user_name, user_email, user_id, tier, date, None)) conn.commit() conn.close() # Send to Google Sheet (via Apps Script) try: requests.post(APPS_SCRIPT_URL, json={ "verification_code": verification_code, "user_name": user_name, "user_email": user_email, "user_id": user_id, "tier": tier, "date": date, "drive_url": "" }, timeout=10) print(f"✅ Sent to Google Sheet: {verification_code}") except Exception as e: print(f"Sheet error: {e}") return {"success": True, "message": "Certificate code stored"} except Exception as e: return {"success": False, "error": str(e)} # ============================================ # UPDATE DRIVE URL (Called after PDF uploaded to Drive) # ============================================ @app.post("/update-drive-url") async def update_drive_url(request: Request): try: data = await request.json() verification_code = data.get('verification_code') drive_url = data.get('drive_url') # Update SQLite conn = sqlite3.connect('domify_users.db') c = conn.cursor() c.execute("UPDATE certificates SET drive_url = ? WHERE verification_code = ?", (drive_url, verification_code)) conn.commit() conn.close() # Also update Google Sheet (optional, send a second webhook) try: # First get the existing row data to update conn = sqlite3.connect('domify_users.db') c = conn.cursor() c.execute("SELECT user_name, user_email, user_id, tier, date FROM certificates WHERE verification_code = ?", (verification_code,)) row = c.fetchone() conn.close() if row: requests.post(APPS_SCRIPT_URL, json={ "verification_code": verification_code, "user_name": row[0], "user_email": row[1], "user_id": row[2], "tier": row[3], "date": row[4], "drive_url": drive_url }, timeout=10) except Exception as e: print(f"Sheet update error: {e}") return {"success": True, "message": "Drive URL updated"} except Exception as e: return {"success": False, "error": str(e)} # ============================================ # VERIFY CERTIFICATE (Returns Drive URL so image can be displayed) # ============================================ @app.get("/verify-certificate") async def verify_certificate(code: str, name: str): conn = sqlite3.connect('domify_users.db') c = conn.cursor() c.execute(""" SELECT user_name, tier, date, drive_url FROM certificates WHERE verification_code = ? AND user_name = ? """, (code.upper(), name)) row = c.fetchone() conn.close() if row: return { "valid": True, "name": row[0], "tier": row[1], "date": row[2], "imageUrl": row[3] # This is the Drive URL to display } else: return {"valid": False, "error": "No matching certificate found"}