Snapshot: full project state
This commit is contained in:
454
app.py
Normal file
454
app.py
Normal file
@@ -0,0 +1,454 @@
|
||||
"""Supernova — a cinematic galaxy-themed SMM reseller panel.
|
||||
|
||||
Customers deposit Bitcoin (BTCPay) into the operator's wallet, then buy social
|
||||
media engagement (followers/likes/views) which the operator fulfils through the
|
||||
upstream SMM panel API. The operator pockets the markup.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import secrets
|
||||
import hmac
|
||||
from functools import wraps
|
||||
|
||||
import requests
|
||||
from flask import (Flask, request, session, redirect, url_for, render_template,
|
||||
jsonify, abort, flash)
|
||||
from panel_client import PanelClient
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# CONFIG
|
||||
# --------------------------------------------------------------------------
|
||||
SMM_API_KEY = os.environ.get("SMM_API_KEY", "7f8a4e57d3568202aa18efe91b48c9c8")
|
||||
MARKUP_PCT = float(os.environ.get("MARKUP_PCT", "100")) # 100% = 2x wholesale
|
||||
|
||||
# BTCPay Server (Greenfield)
|
||||
BTCPAY_URL = os.environ.get("BTCPAY_URL", "https://10.30.20.140")
|
||||
BTCPAY_API_KEY = os.environ.get("BTCPAY_API_KEY", "")
|
||||
BTCPAY_STORE_ID = os.environ.get("BTCPAY_STORE_ID", "")
|
||||
BTCPAY_WEBHOOK_SECRET = os.environ.get("BTCPAY_WEBHOOK_SECRET", "")
|
||||
BTCPAY_PUBLIC = os.environ.get("BTCPAY_PUBLIC", "https://btcpay.thetempleofdoom.com")
|
||||
SITE_URL = os.environ.get("SITE_URL", "https://supernova.thetempleofdoom.com")
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "supernova.db")
|
||||
SERVICE_CACHE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "services.json")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32))
|
||||
|
||||
panel = PanelClient(SMM_API_KEY)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# DB
|
||||
# --------------------------------------------------------------------------
|
||||
def db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def init_db():
|
||||
c = db()
|
||||
c.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
balance REAL NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
service_id INTEGER NOT NULL,
|
||||
service_name TEXT NOT NULL,
|
||||
link TEXT NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
cost REAL NOT NULL,
|
||||
panel_order_id INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS deposits (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
invoice_id TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
""")
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Auth
|
||||
# --------------------------------------------------------------------------
|
||||
def hash_password(pw, salt=None):
|
||||
salt = salt or secrets.token_hex(16)
|
||||
dk = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), 200_000)
|
||||
return f"{salt}${dk.hex()}"
|
||||
|
||||
|
||||
def verify_password(pw, stored):
|
||||
salt, _ = stored.split("$", 1)
|
||||
return hmac.compare_digest(hash_password(pw, salt), stored)
|
||||
|
||||
|
||||
def current_user():
|
||||
uid = session.get("uid")
|
||||
if not uid:
|
||||
return None
|
||||
c = db()
|
||||
u = c.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()
|
||||
c.close()
|
||||
return u
|
||||
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def wrap(*a, **kw):
|
||||
if not current_user():
|
||||
return redirect(url_for("login", next=request.path))
|
||||
return f(*a, **kw)
|
||||
return wrap
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Service cache (avoid hitting the panel API every page load)
|
||||
# --------------------------------------------------------------------------
|
||||
def get_services(force=False):
|
||||
if not force and os.path.exists(SERVICE_CACHE):
|
||||
if time.time() - os.path.getmtime(SERVICE_CACHE) < 900: # 15 min TTL
|
||||
with open(SERVICE_CACHE) as f:
|
||||
return json.load(f)
|
||||
svcs = panel.services()
|
||||
with open(SERVICE_CACHE, "w") as f:
|
||||
json.dump(svcs, f)
|
||||
return svcs
|
||||
|
||||
|
||||
def wholesale_cost(rate, quantity):
|
||||
return float(rate) * quantity / 1000.0
|
||||
|
||||
|
||||
def retail_price(rate, quantity):
|
||||
return wholesale_cost(rate, quantity) * (1 + MARKUP_PCT / 100.0)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# BTCPay
|
||||
# --------------------------------------------------------------------------
|
||||
def btcpay_headers():
|
||||
return {"Authorization": f"token {BTCPAY_API_KEY}",
|
||||
"Content-Type": "application/json"}
|
||||
|
||||
|
||||
def create_invoice(amount_usd, order_id, buyer_email):
|
||||
# price in USD; BTCPay converts to BTC at its rate
|
||||
payload = {
|
||||
"amount": str(round(amount_usd, 2)),
|
||||
"currency": "USD",
|
||||
"checkout": {"redirectURL": SITE_URL + "/dashboard"},
|
||||
"metadata": {"orderId": str(order_id),
|
||||
"buyerEmail": buyer_email},
|
||||
}
|
||||
r = requests.post(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE_ID}/invoices",
|
||||
headers=btcpay_headers(), json=payload, timeout=20, verify=False)
|
||||
r.raise_for_status()
|
||||
inv = r.json()
|
||||
# rewrite checkoutLink from LAN IP to public host (customer-facing)
|
||||
link = inv.get("checkoutLink", "")
|
||||
if link and link.startswith(BTCPAY_URL):
|
||||
inv["checkoutLink"] = BTCPAY_PUBLIC + link[len(BTCPAY_URL):]
|
||||
return inv
|
||||
|
||||
|
||||
def get_invoice(invoice_id):
|
||||
r = requests.get(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE_ID}/invoices/{invoice_id}",
|
||||
headers=btcpay_headers(), timeout=20, verify=False)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Routes
|
||||
# --------------------------------------------------------------------------
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html", user=current_user())
|
||||
|
||||
|
||||
@app.route("/robots.txt")
|
||||
def robots():
|
||||
body = ("User-agent: *\n"
|
||||
"Allow: /\n"
|
||||
"Sitemap: https://supernova.thetempleofdoom.com/sitemap.xml\n")
|
||||
return body, 200, {"Content-Type": "text/plain"}
|
||||
|
||||
|
||||
@app.route("/sitemap.xml")
|
||||
def sitemap():
|
||||
pages = [
|
||||
("/", "1.0", "weekly"),
|
||||
("/services", "0.9", "daily"),
|
||||
("/signup", "0.5", "monthly"),
|
||||
("/login", "0.3", "monthly"),
|
||||
]
|
||||
urls = "".join(
|
||||
f"<url><loc>https://supernova.thetempleofdoom.com{p}</loc>"
|
||||
f"<changefreq>{freq}</changefreq><priority>{pri}</priority></url>"
|
||||
for p, pri, freq in pages
|
||||
)
|
||||
xml = ('<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
||||
f"{urls}</urlset>")
|
||||
return xml, 200, {"Content-Type": "application/xml"}
|
||||
|
||||
|
||||
@app.route("/bg-demo")
|
||||
def bg_demo():
|
||||
theme = request.args.get("theme", "cosmos")
|
||||
themes = ["starfield", "cosmos", "blackhole", "geometric", "nebula", "particles", "waves", "grid"]
|
||||
return render_template("bg-demo.html", user=current_user(), theme=theme, themes=themes)
|
||||
|
||||
|
||||
@app.route("/signup", methods=["GET", "POST"])
|
||||
def signup():
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email", "").strip().lower()
|
||||
pw = request.form.get("password", "")
|
||||
if len(pw) < 8:
|
||||
flash("Password must be at least 8 characters.", "error")
|
||||
return redirect(url_for("signup"))
|
||||
c = db()
|
||||
try:
|
||||
c.execute("INSERT INTO users (email, password_hash, created_at) VALUES (?,?,?)",
|
||||
(email, hash_password(pw), int(time.time())))
|
||||
c.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
c.close()
|
||||
flash("That email is already registered.", "error")
|
||||
return redirect(url_for("signup"))
|
||||
uid = c.execute("SELECT id FROM users WHERE email=?", (email,)).fetchone()["id"]
|
||||
c.close()
|
||||
session["uid"] = uid
|
||||
return redirect(url_for("dashboard"))
|
||||
return render_template("auth.html", mode="signup", user=None)
|
||||
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email", "").strip().lower()
|
||||
pw = request.form.get("password", "")
|
||||
c = db()
|
||||
u = c.execute("SELECT * FROM users WHERE email=?", (email,)).fetchone()
|
||||
c.close()
|
||||
if u and verify_password(pw, u["password_hash"]):
|
||||
session["uid"] = u["id"]
|
||||
nxt = request.args.get("next") or url_for("dashboard")
|
||||
return redirect(nxt)
|
||||
flash("Invalid email or password.", "error")
|
||||
return render_template("auth.html", mode="login", user=None)
|
||||
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.route("/services")
|
||||
def services():
|
||||
svcs = get_services()
|
||||
# group by category
|
||||
cats = {}
|
||||
for s in svcs:
|
||||
cats.setdefault(s["category"], []).append(s)
|
||||
return render_template("services.html", user=current_user(), cats=cats)
|
||||
|
||||
|
||||
@app.route("/order/<int:service_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def order(service_id):
|
||||
svcs = {s["service"]: s for s in get_services()}
|
||||
s = svcs.get(service_id)
|
||||
if not s:
|
||||
abort(404)
|
||||
if request.method == "POST":
|
||||
link = request.form.get("link", "").strip()
|
||||
quantity = int(request.form.get("quantity", 0))
|
||||
cost = retail_price(s["rate"], quantity)
|
||||
if not link or quantity < s["min"]:
|
||||
flash(f"Enter a valid link and quantity (min {s['min']}).", "error")
|
||||
return redirect(url_for("order", service_id=service_id))
|
||||
u = current_user()
|
||||
if u["balance"] < cost:
|
||||
flash("Insufficient balance — deposit more BTC first.", "error")
|
||||
return redirect(url_for("deposit"))
|
||||
# charge + place upstream order
|
||||
try:
|
||||
res = panel.place_order(service_id, link, quantity)
|
||||
panel_oid = res.get("order")
|
||||
except Exception as e:
|
||||
flash(f"Upstream order failed: {e}", "error")
|
||||
return redirect(url_for("order", service_id=service_id))
|
||||
c = db()
|
||||
c.execute("UPDATE users SET balance = balance - ? WHERE id=?",
|
||||
(cost, u["id"]))
|
||||
c.execute("""INSERT INTO orders
|
||||
(user_id, service_id, service_name, link, quantity, cost,
|
||||
panel_order_id, status, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||||
(u["id"], service_id, s["name"], link, quantity, cost,
|
||||
panel_oid, "pending", int(time.time())))
|
||||
c.commit()
|
||||
c.close()
|
||||
flash(f"Order placed! ${cost:.2f} — it'll start delivering shortly.", "success")
|
||||
return redirect(url_for("orders") + "?launched=1")
|
||||
return render_template("order.html", user=current_user(), s=s,
|
||||
markup=MARKUP_PCT)
|
||||
|
||||
|
||||
@app.route("/orders")
|
||||
@login_required
|
||||
def orders():
|
||||
u = current_user()
|
||||
c = db()
|
||||
rows = c.execute("SELECT * FROM orders WHERE user_id=? ORDER BY id DESC LIMIT 100",
|
||||
(u["id"],)).fetchall()
|
||||
c.close()
|
||||
return render_template("orders.html", user=u, orders=rows)
|
||||
|
||||
|
||||
@app.route("/dashboard")
|
||||
@login_required
|
||||
def dashboard():
|
||||
u = current_user()
|
||||
c = db()
|
||||
orders = c.execute("SELECT * FROM orders WHERE user_id=? ORDER BY id DESC LIMIT 10",
|
||||
(u["id"],)).fetchall()
|
||||
deposits = c.execute("SELECT * FROM deposits WHERE user_id=? ORDER BY id DESC LIMIT 10",
|
||||
(u["id"],)).fetchall()
|
||||
c.close()
|
||||
return render_template("dashboard.html", user=u, orders=orders, deposits=deposits)
|
||||
|
||||
|
||||
@app.route("/deposit", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def deposit():
|
||||
u = current_user()
|
||||
if request.method == "POST":
|
||||
amount = float(request.form.get("amount", 0))
|
||||
if amount <= 0:
|
||||
flash("Enter a valid amount.", "error")
|
||||
return redirect(url_for("deposit"))
|
||||
c = db()
|
||||
cur = c.execute("""INSERT INTO deposits (user_id, amount, status, created_at)
|
||||
VALUES (?,?,'pending',?)""",
|
||||
(u["id"], amount, int(time.time())))
|
||||
c.commit()
|
||||
dep_id = cur.lastrowid
|
||||
c.close()
|
||||
inv = create_invoice(amount, dep_id, u["email"])
|
||||
c = db()
|
||||
c.execute("UPDATE deposits SET invoice_id=? WHERE id=?",
|
||||
(inv["id"], dep_id))
|
||||
c.commit()
|
||||
c.close()
|
||||
return redirect(inv["checkoutLink"])
|
||||
return render_template("deposit.html", user=u)
|
||||
|
||||
|
||||
@app.route("/webhook/btcpay", methods=["POST"])
|
||||
def btcpay_webhook():
|
||||
# verify signature
|
||||
sig = request.headers.get("BTCPay-Sig", "")
|
||||
body = request.get_data()
|
||||
expected = "sha256=" + hmac.new(BTCPAY_WEBHOOK_SECRET.encode(), body,
|
||||
hashlib.sha256).hexdigest()
|
||||
if BTCPAY_WEBHOOK_SECRET and not hmac.compare_digest(sig, expected):
|
||||
return "bad signature", 401
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
inv_id = data.get("invoiceId")
|
||||
if not inv_id:
|
||||
return "ok", 200
|
||||
|
||||
# re-fetch invoice to confirm status (don't trust the webhook blindly)
|
||||
try:
|
||||
inv = get_invoice(inv_id)
|
||||
except Exception:
|
||||
return "ok", 200
|
||||
if inv.get("status") != "Settled":
|
||||
return "ok", 200
|
||||
|
||||
dep_id = (inv.get("metadata") or {}).get("orderId")
|
||||
amount = float(inv.get("amount", 0))
|
||||
c = db()
|
||||
dep = c.execute("SELECT * FROM deposits WHERE id=?", (dep_id,)).fetchone()
|
||||
if dep and dep["status"] != "credited":
|
||||
c.execute("UPDATE deposits SET status='credited' WHERE id=?", (dep_id,))
|
||||
c.execute("UPDATE users SET balance = balance + ? WHERE id=?",
|
||||
(amount, dep["user_id"]))
|
||||
c.commit()
|
||||
c.close()
|
||||
return "ok", 200
|
||||
|
||||
|
||||
@app.route("/api/quote/<int:service_id>/<int:quantity>")
|
||||
def api_quote(service_id, quantity):
|
||||
svcs = {s["service"]: s for s in get_services()}
|
||||
s = svcs.get(service_id)
|
||||
if not s:
|
||||
return jsonify({"error": "unknown service"}), 404
|
||||
return jsonify({
|
||||
"quantity": quantity,
|
||||
"wholesale": round(wholesale_cost(s["rate"], quantity), 4),
|
||||
"retail": round(retail_price(s["rate"], quantity), 2),
|
||||
"rate": s["rate"],
|
||||
"min": s["min"],
|
||||
"max": s["max"],
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/orders-status")
|
||||
@login_required
|
||||
def api_orders_status():
|
||||
"""Live delivery status for the dashboard ticker."""
|
||||
u = current_user()
|
||||
c = db()
|
||||
rows = c.execute("SELECT * FROM orders WHERE user_id=? ORDER BY id DESC LIMIT 8",
|
||||
(u["id"],)).fetchall()
|
||||
c.close()
|
||||
out = []
|
||||
for o in rows:
|
||||
status = o["status"]
|
||||
remains = 0
|
||||
delivered = o["quantity"]
|
||||
if o["panel_order_id"]:
|
||||
try:
|
||||
st = panel.order_status(o["panel_order_id"])
|
||||
status = (st.get("status") or o["status"]).lower().replace(" ", "")
|
||||
remains = int(st.get("remains") or 0)
|
||||
delivered = max(0, o["quantity"] - remains)
|
||||
except Exception:
|
||||
pass
|
||||
out.append({
|
||||
"id": o["id"],
|
||||
"name": o["service_name"],
|
||||
"quantity": o["quantity"],
|
||||
"delivered": delivered,
|
||||
"status": status,
|
||||
})
|
||||
return jsonify({"orders": out})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
Reference in New Issue
Block a user