394 lines
20 KiB
Python
394 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Rigel — no-KYC SOCKS5 proxy shop.
|
|
Sells access to residential/mobile/datacenter proxies. Bitcoin via BTCPay.
|
|
|
|
Auth: no-KYC (username + email + password, no ID). Captcha-lite (math challenge).
|
|
Inventory: proxied through gost SOCKS5 frontends with per-user auth.
|
|
"""
|
|
import os, sqlite3, json, hmac, hashlib, time, uuid, secrets
|
|
from datetime import datetime, timedelta
|
|
from flask import Flask, request, jsonify, g, render_template_string, redirect, session
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32))
|
|
|
|
DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rigel.db")
|
|
|
|
# ── config ──────────────────────────────────────────────────────────────
|
|
# Proxy inventory: each location is a gost frontend. We issue per-user creds.
|
|
# The actual upstream SOCKS5 endpoints (Nord CTs) are hidden behind a gost
|
|
# auth layer so customers never see the raw CT IPs.
|
|
LOCATIONS = {
|
|
"tokyo": {"name": "Tokyo, JP", "upstream": "10.30.20.154:1080", "type": "datacenter"},
|
|
"london": {"name": "London, UK", "upstream": "10.30.20.71:1080", "type": "datacenter"},
|
|
"sydney": {"name": "Sydney, AU", "upstream": "10.30.20.189:1080", "type": "datacenter"},
|
|
# IPRoyal residential + mobile are added when creds are provisioned
|
|
"residential": {"name": "Residential (rotating)", "upstream": "geo.iproyal.com:12321", "type": "residential"},
|
|
"mobile": {"name": "Mobile 4G (sticky)", "upstream": "us.4g.iproyal.com:7001", "type": "mobile"},
|
|
}
|
|
|
|
PLANS = {
|
|
"day": {"label": "1 Day", "hours": 24, "sats": 8000},
|
|
"week": {"label": "1 Week", "hours": 168, "sats": 45000},
|
|
"month": {"label": "1 Month", "hours": 720, "sats": 150000},
|
|
}
|
|
|
|
BTCPAY_URL = os.environ.get("BTCPAY_URL", "https://10.30.20.140")
|
|
BTCPAY_STORE = os.environ.get("BTCPAY_STORE_ID", "")
|
|
BTCPAY_TOKEN = os.environ.get("BTCPAY_API_KEY", "")
|
|
|
|
# Public SOCKS5 endpoint customers connect to (authenticated frontend on CT158)
|
|
PROXY_HOST = os.environ.get("PROXY_PUBLIC_HOST", "10.30.20.116")
|
|
PROXY_PORT = os.environ.get("PROXY_PUBLIC_PORT", "1081")
|
|
|
|
def db():
|
|
con = getattr(g, "_db", None)
|
|
if con is None:
|
|
con = sqlite3.connect(DB)
|
|
con.row_factory = sqlite3.Row
|
|
g._db = con
|
|
return con
|
|
|
|
@app.teardown_appcontext
|
|
def close_db(exc):
|
|
con = getattr(g, "_db", None)
|
|
if con is not None:
|
|
con.close()
|
|
|
|
def init_db():
|
|
con = sqlite3.connect(DB)
|
|
con.executescript("""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
email TEXT,
|
|
password_hash TEXT NOT NULL,
|
|
balance_sats INTEGER DEFAULT 0,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
location TEXT NOT NULL,
|
|
plan TEXT NOT NULL,
|
|
sats_paid INTEGER NOT NULL,
|
|
starts_at TEXT,
|
|
expires_at TEXT,
|
|
proxy_user TEXT,
|
|
proxy_pass TEXT,
|
|
status TEXT DEFAULT 'pending', -- pending|active|expired
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS invoices (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
btcpay_invoice_id TEXT,
|
|
amount_sats INTEGER,
|
|
status TEXT DEFAULT 'new', -- new|paid|expired
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
""")
|
|
con.commit()
|
|
con.close()
|
|
|
|
def hash_pw(pw, salt=None):
|
|
salt = salt or secrets.token_hex(16)
|
|
return salt + ":" + hmac.new(salt.encode(), pw.encode(), hashlib.sha256).hexdigest()
|
|
|
|
def check_pw(pw, stored):
|
|
salt, digest = stored.split(":", 1)
|
|
return hmac.new(salt.encode(), pw.encode(), hashlib.sha256).hexdigest() == digest
|
|
|
|
def current_user():
|
|
uid = session.get("uid")
|
|
if not uid:
|
|
return None
|
|
return db().execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()
|
|
|
|
# ── routes ──────────────────────────────────────────────────────────────
|
|
@app.route("/")
|
|
def index():
|
|
user = current_user()
|
|
if not user:
|
|
return render_template_string(AUTH_HTML, mode="login", error="")
|
|
return render_template_string(DASH_HTML, user=user, locations=LOCATIONS, plans=PLANS,
|
|
proxy_host=PROXY_HOST, proxy_port=PROXY_PORT)
|
|
|
|
@app.route("/health")
|
|
def health():
|
|
return jsonify({"status": "ok", "service": "rigel"})
|
|
|
|
@app.route("/api/register", methods=["POST"])
|
|
def register():
|
|
d = request.get_json(force=True) if request.is_json else request.form
|
|
username = (d.get("username") or "").strip()
|
|
email = (d.get("email") or "").strip()
|
|
pw = d.get("password") or ""
|
|
captcha = d.get("captcha") or ""
|
|
if not username or not pw or len(username) > 25 or len(pw) < 6:
|
|
return jsonify({"error": "username + password (6+ chars) required"}), 400
|
|
if not check_captcha(captcha):
|
|
return jsonify({"error": "wrong captcha"}), 400
|
|
con = db()
|
|
try:
|
|
con.execute("INSERT INTO users (username, email, password_hash) VALUES (?,?,?)",
|
|
(username, email, hash_pw(pw)))
|
|
con.commit()
|
|
except sqlite3.IntegrityError:
|
|
return jsonify({"error": "username taken"}), 409
|
|
return jsonify({"ok": True})
|
|
|
|
@app.route("/api/login", methods=["POST"])
|
|
def login():
|
|
d = request.get_json(force=True) if request.is_json else request.form
|
|
username = (d.get("username") or "").strip()
|
|
pw = d.get("password") or ""
|
|
captcha = d.get("captcha") or ""
|
|
if not check_captcha(captcha):
|
|
return jsonify({"error": "wrong captcha"}), 400
|
|
user = db().execute("SELECT * FROM users WHERE username=?", (username,)).fetchone()
|
|
if not user or not check_pw(pw, user["password_hash"]):
|
|
return jsonify({"error": "bad credentials"}), 401
|
|
session["uid"] = user["id"]
|
|
return jsonify({"ok": True})
|
|
|
|
@app.route("/api/logout", methods=["POST"])
|
|
def logout():
|
|
session.pop("uid", None)
|
|
return jsonify({"ok": True})
|
|
|
|
# Captcha-lite: math challenge rendered as text (no external captcha service)
|
|
@app.route("/api/captcha")
|
|
def captcha():
|
|
a, b = secrets.randbelow(9) + 1, secrets.randbelow(9) + 1
|
|
session["captcha"] = str(a + b)
|
|
return jsonify({"question": f"What is {a} + {b}?", "id": "c1"})
|
|
|
|
def check_captcha(ans):
|
|
return (ans or "").strip() == session.get("captcha", "")
|
|
|
|
@app.route("/api/buy", methods=["POST"])
|
|
def buy():
|
|
user = current_user()
|
|
if not user:
|
|
return jsonify({"error": "login required"}), 401
|
|
d = request.get_json(force=True) if request.is_json else request.form
|
|
location = d.get("location", "")
|
|
plan = d.get("plan", "")
|
|
if location not in LOCATIONS or plan not in PLANS:
|
|
return jsonify({"error": "bad location/plan"}), 400
|
|
sats = PLANS[plan]["sats"]
|
|
# create BTCPay invoice
|
|
invoice_id, checkout = create_btcpay_invoice(user, sats, f"Rigel {LOCATIONS[location]['name']} {PLANS[plan]['label']}")
|
|
con = db()
|
|
cur = con.execute("INSERT INTO subscriptions (user_id, location, plan, sats_paid, proxy_user) VALUES (?,?,?,?,?)",
|
|
(user["id"], location, plan, sats, f"u{user['id']}{secrets.token_hex(4)}"))
|
|
con.execute("INSERT INTO invoices (user_id, btcpay_invoice_id, amount_sats) VALUES (?,?,?)",
|
|
(user["id"], invoice_id, sats))
|
|
con.commit()
|
|
return jsonify({"ok": True, "invoice_id": invoice_id, "checkout": checkout, "sats": sats})
|
|
|
|
def create_btcpay_invoice(user, sats, desc):
|
|
if not BTCPAY_STORE or not BTCPAY_TOKEN:
|
|
# fallback: no BTCPay configured — return a placeholder
|
|
return "local-" + uuid.uuid4().hex[:12], None
|
|
import urllib.request, ssl as _ssl
|
|
ctx = _ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = _ssl.CERT_NONE
|
|
body = json.dumps({"amount": str(sats/1e8), "currency": "BTC", "metadata": {"desc": desc, "user": user["username"]}})
|
|
req = urllib.request.Request(f"{BTCPAY_URL}/api/v1/stores/{BTCPAY_STORE}/invoices",
|
|
data=body.encode(), headers={"Authorization": f"token {BTCPAY_TOKEN}", "Content-Type": "application/json"}, method="POST")
|
|
r = urllib.request.urlopen(req, timeout=20, context=ctx)
|
|
inv = json.loads(r.read())
|
|
return inv["id"], inv.get("checkoutLink")
|
|
|
|
@app.route("/api/subscriptions")
|
|
def subscriptions():
|
|
user = current_user()
|
|
if not user:
|
|
return jsonify({"error": "login required"}), 401
|
|
subs = db().execute("SELECT * FROM subscriptions WHERE user_id=? ORDER BY id DESC", (user["id"],)).fetchall()
|
|
return jsonify([dict(s) for s in subs])
|
|
|
|
@app.route("/api/status")
|
|
def status():
|
|
user = current_user()
|
|
if not user:
|
|
return jsonify({"authenticated": False})
|
|
return jsonify({"authenticated": True, "username": user["username"], "balance_sats": user["balance_sats"]})
|
|
|
|
# ── BTCPay webhook: activate subscription on payment ─────────────────────
|
|
WEBHOOK_SECRET = os.environ.get("BTCPAY_WEBHOOK_SECRET", "")
|
|
|
|
@app.route("/api/btcpay/webhook", methods=["POST"])
|
|
def btcpay_webhook():
|
|
# optional HMAC verification (raw body)
|
|
if WEBHOOK_SECRET:
|
|
raw = request.get_data()
|
|
sig = request.headers.get("BTCPay-Sig", "")
|
|
expected = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
|
|
if not hmac.compare_digest(sig, expected):
|
|
return jsonify({"error": "bad signature"}), 401
|
|
payload = request.get_json(force=True, silent=True) or {}
|
|
etype = payload.get("type", "")
|
|
invoice_id = payload.get("invoiceId", "")
|
|
status = payload.get("status", "")
|
|
|
|
if etype in ("InvoiceSettled", "InvoiceReceivedPayment") and invoice_id:
|
|
# find matching invoice -> subscription
|
|
con = sqlite3.connect(DB)
|
|
con.row_factory = sqlite3.Row
|
|
inv = con.execute("SELECT * FROM invoices WHERE btcpay_invoice_id=? AND status!='paid'", (invoice_id,)).fetchone()
|
|
if inv:
|
|
con.execute("UPDATE invoices SET status='paid' WHERE btcpay_invoice_id=?", (invoice_id,))
|
|
# activate subscription + issue proxy creds
|
|
sub = con.execute("SELECT * FROM subscriptions WHERE user_id=? AND status='pending' ORDER BY id DESC LIMIT 1",
|
|
(inv["user_id"],)).fetchone()
|
|
if sub:
|
|
proxy_user = sub["proxy_user"] or f"u{inv['user_id']}{secrets.token_hex(4)}"
|
|
proxy_pass = secrets.token_hex(12)
|
|
hours = PLANS.get(sub["plan"], {}).get("hours", 24)
|
|
now = datetime.utcnow()
|
|
expires = now + timedelta(hours=hours)
|
|
con.execute("""UPDATE subscriptions SET status='active', proxy_user=?, proxy_pass=?,
|
|
starts_at=?, expires_at=? WHERE id=?""",
|
|
(proxy_user, proxy_pass, now.isoformat(), expires.isoformat(), sub["id"]))
|
|
# provision the proxy auth upstream (gost) — best-effort
|
|
provision_proxy(sub["location"], proxy_user, proxy_pass)
|
|
con.commit()
|
|
con.close()
|
|
return jsonify({"ok": True})
|
|
|
|
def provision_proxy(location, user, pw):
|
|
"""No per-node writes needed: the authenticated SOCKS5 frontend
|
|
(rigel-proxy.service on CT158) validates every connection live against the
|
|
subscriptions table, so persisting the row IS the provisioning step.
|
|
The upstream exits are dumb unauthenticated relays on the LAN."""
|
|
up = LOCATIONS.get(location, {}).get("upstream", "?")
|
|
app.logger.info(f"provisioned {location} (upstream {up}) for {user}")
|
|
|
|
# ── templates ───────────────────────────────────────────────────────────
|
|
AUTH_HTML = r"""
|
|
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Rigel — SOCKS5 Proxies</title>
|
|
<style>
|
|
:root{--bg:#0a0e1a;--card:#12182b;--accent:#4f8cff;--text:#e6ebf5;--muted:#8b95ad;--ok:#2ecc71}
|
|
*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center}
|
|
.card{background:var(--card);border:1px solid #1e2742;border-radius:14px;padding:34px;width:100%;max-width:400px;box-shadow:0 10px 40px rgba(0,0,0,.4)}
|
|
h1{font-size:26px;margin:0 0 4px;color:#fff} .sub{color:var(--muted);font-size:13px;margin:0 0 24px}
|
|
.star{color:var(--accent)} label{display:block;font-size:12px;color:var(--muted);margin:14px 0 4px}
|
|
input{width:100%;padding:11px 12px;border-radius:8px;border:1px solid #26304f;background:#0d1325;color:var(--text);font-size:14px}
|
|
input:focus{outline:none;border-color:var(--accent)}
|
|
button{width:100%;padding:12px;margin-top:20px;border:0;border-radius:8px;background:var(--accent);color:#fff;font-size:15px;font-weight:600;cursor:pointer}
|
|
button.alt{background:transparent;border:1px solid #26304f;color:var(--text);margin-top:10px}
|
|
a{color:var(--accent);text-decoration:none;font-size:13px;display:block;text-align:center;margin-top:14px}
|
|
.err{color:#e74c3c;font-size:13px;margin-top:10px;min-height:16px}
|
|
.cap{display:flex;gap:10px;align-items:center;margin-top:14px}.cap .q{font-size:14px;color:var(--text);flex:1}.cap input{width:90px}
|
|
</style></head><body><div class="card">
|
|
<h1><span class="star">✦</span> Rigel</h1><p class="sub">No-KYC SOCKS5 & residential proxies. Pay in Bitcoin.</p>
|
|
<div id="body"></div>
|
|
<script>
|
|
let mode="{{mode}}";
|
|
function esc(s){return s.replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
|
async function captcha(){let r=await fetch('/api/captcha').then(x=>x.json());document.getElementById('cq').innerText=r.question;}
|
|
function loginForm(){
|
|
return `<label>Username</label><input id="u" maxlength="25">
|
|
<label>Password</label><input id="p" type="password" maxlength="30">
|
|
<div class="cap"><span class="q" id="cq"></span><input id="c" maxlength="6" placeholder="answer"></div>
|
|
<button onclick="doLogin()">Login</button>
|
|
<a href="javascript:regForm()">Create an account</a><div class="err" id="err"></div>`;
|
|
}
|
|
function regForm(){
|
|
return `<label>Username</label><input id="u" maxlength="25">
|
|
<label>Email <span style="color:#5a6480">(optional, for restore)</span></label><input id="e" maxlength="75">
|
|
<label>Password</label><input id="p" type="password" maxlength="30">
|
|
<div class="cap"><span class="q" id="cq"></span><input id="c" maxlength="6" placeholder="answer"></div>
|
|
<button onclick="doRegister()">Create account</button>
|
|
<a href="javascript:loginForm()">Already have an account?</a><div class="err" id="err"></div>`;
|
|
}
|
|
function err(s){document.getElementById('err').innerText=s;}
|
|
async function doLogin(){
|
|
let r=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('u').value,password:document.getElementById('p').value,captcha:document.getElementById('c').value})});
|
|
if(r.ok){location.reload();}else{let j=await r.json();err(j.error||'login failed');captcha();}
|
|
}
|
|
async function doRegister(){
|
|
let r=await fetch('/api/register',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:document.getElementById('u').value,email:document.getElementById('e').value,password:document.getElementById('p').value,captcha:document.getElementById('c').value})});
|
|
if(r.ok){loginForm();err('account created — log in');captcha();}else{let j=await r.json();err(j.error||'registration failed');captcha();}
|
|
}
|
|
if(mode==='login'){document.getElementById('body').innerHTML=loginForm();captcha();}
|
|
</script></div></body></html>
|
|
"""
|
|
|
|
DASH_HTML = r"""
|
|
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Rigel — Dashboard</title>
|
|
<style>
|
|
:root{--bg:#0a0e1a;--card:#12182b;--accent:#4f8cff;--text:#e6ebf5;--muted:#8b95ad}
|
|
*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
|
header{display:flex;justify-content:space-between;align-items:center;padding:16px 28px;background:var(--card);border-bottom:1px solid #1e2742}
|
|
header h1{margin:0;font-size:20px}header h1 span{color:var(--accent)}
|
|
header button{background:transparent;border:1px solid #26304f;color:var(--text);padding:8px 14px;border-radius:8px;cursor:pointer}
|
|
main{padding:28px;max-width:900px;margin:0 auto}
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px}
|
|
.loc{background:var(--card);border:1px solid #1e2742;border-radius:12px;padding:20px}
|
|
.loc h3{margin:0 0 4px;font-size:17px;color:#fff}.loc .type{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
|
|
.loc .plans{margin-top:14px}.loc .plan{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-top:1px solid #1e2742;font-size:13px}
|
|
.loc button{background:var(--accent);color:#fff;border:0;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px}
|
|
.subs{margin-top:32px}.subs h2{font-size:17px}.subs table{width:100%;border-collapse:collapse;font-size:13px}
|
|
.subs th,.subs td{padding:10px;text-align:left;border-bottom:1px solid #1e2742}.subs th{color:var(--muted);font-weight:500}
|
|
.badge{padding:3px 8px;border-radius:10px;font-size:11px}.active{background:#14321f;color:#2ecc71}.pending{background:#332a14;color:#f1c40f}.expired{background:#33171a;color:#e74c3c}
|
|
.subcard{background:var(--card);border:1px solid #1e2742;border-radius:12px;padding:18px;margin-top:14px}
|
|
.srow{font-size:15px;margin-bottom:6px}
|
|
.lbl{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-top:10px}
|
|
code{display:block;background:#0d1325;border:1px solid #26304f;border-radius:6px;padding:8px 10px;font-size:12px;color:#9fd0ff;word-break:break-all;margin-top:3px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
</style></head><body>
|
|
<header><h1><span>✦</span> Rigel <span style="color:var(--muted);font-weight:400;font-size:13px">— {{user["username"]}}</span></h1><button onclick="logout()">Logout</button></header>
|
|
<main>
|
|
<div class="grid">
|
|
{% for key, loc in locations.items() %}
|
|
<div class="loc">
|
|
<h3>{{loc["name"]}}</h3><div class="type">{{loc["type"]}}</div>
|
|
<div class="plans">
|
|
{% for pk, p in plans.items() %}
|
|
<div class="plan"><span>{{p["label"]}}</span><span><b>{{p["sats"]}}</b> sats</span><button onclick="buy('{{key}}','{{pk}}')">Buy</button></div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="subs"><h2>Your subscriptions</h2><div id="subs">Loading…</div></div>
|
|
</main>
|
|
<script>
|
|
async function buy(loc, plan){
|
|
let r=await fetch('/api/buy',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:loc,plan:plan})});
|
|
let j=await r.json();
|
|
if(j.ok && j.checkout){window.location=j.checkout;}else{alert(j.error||'error');}
|
|
}
|
|
async function logout(){await fetch('/api/logout',{method:'POST'});location.reload();}
|
|
async function loadSubs(){
|
|
let r=await fetch('/api/subscriptions').then(x=>x.json());
|
|
if(!r.length){document.getElementById('subs').innerHTML='<p style="color:var(--muted)">No subscriptions yet.</p>';return;}
|
|
let h='';
|
|
for(const s of r){
|
|
h+=`<div class="subcard"><div class="srow"><b style="text-transform:capitalize">${s.location}</b> · ${s.plan} · <span class="badge ${s.status}">${s.status}</span></div>`;
|
|
if(s.status==='active'&&s.proxy_user){
|
|
h+=`<div class="lbl">SOCKS5 endpoint</div><code>{{proxy_host}}:{{proxy_port}}</code>`
|
|
+ `<div class="lbl">Username</div><code>${s.proxy_user}</code>`
|
|
+ `<div class="lbl">Password</div><code>${s.proxy_pass||'\u2014'}</code>`
|
|
+ `<div class="lbl">Expires (UTC)</div><code>${s.expires_at||'\u2014'}</code>`
|
|
+ `<div class="lbl">Test it</div><code>curl --socks5-hostname ${s.proxy_user}:${s.proxy_pass}@{{proxy_host}}:{{proxy_port}} https://api.ipify.org</code>`;
|
|
} else {
|
|
h+=`<div style="color:var(--muted);font-size:13px;margin-top:8px">Awaiting payment confirmation\u2026 credentials appear here automatically.</div>`;
|
|
}
|
|
h+='</div>';
|
|
}
|
|
document.getElementById('subs').innerHTML=h;
|
|
}
|
|
loadSubs();
|
|
</script></body></html>
|
|
"""
|
|
|
|
if __name__ == "__main__":
|
|
init_db()
|
|
app.run(host="0.0.0.0", port=5000, debug=False)
|