signup / main.py
Domify's picture
Update main.py
02c5ecf verified
# 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"}