455 lines
15 KiB
Python
455 lines
15 KiB
Python
"""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)
|