Wallet system (sats ledger + deposit + spend), HydraProxy fulfillment queue, paid IP-quality scorer (85/100 verified)
This commit is contained in:
173
app.py
173
app.py
@@ -14,12 +14,14 @@ from flask import (Flask, abort, flash, jsonify, redirect, render_template,
|
||||
import db
|
||||
import iproyal
|
||||
import hydraproxy
|
||||
import quality
|
||||
|
||||
CFG = db.CFG
|
||||
app = Flask(__name__)
|
||||
app.secret_key = CFG["secret_key"]
|
||||
app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # mitigate CSRF on modern browsers
|
||||
app.config["SESSION_COOKIE_HTTPONLY"] = True # block JS session theft (XSS)
|
||||
db.init_db() # ensure schema + migrations on gunicorn start
|
||||
|
||||
# simple SQLite-backed rate limiter (shared across gunicorn workers)
|
||||
def _client_ip():
|
||||
@@ -196,20 +198,24 @@ def plans():
|
||||
@app.route("/buy/<int:plan_id>", methods=["POST"])
|
||||
@login_required
|
||||
def buy(plan_id):
|
||||
u = current_user()
|
||||
d = db.get_db()
|
||||
plan = d.execute("SELECT * FROM plans WHERE id=?", (plan_id,)).fetchone()
|
||||
d.close()
|
||||
if not plan:
|
||||
abort(404)
|
||||
order_id = db.random_token(12)
|
||||
price = plan["price_sats"]
|
||||
if db.get_balance_sats(u["id"]) < price:
|
||||
flash("Insufficient wallet balance. Deposit first.", "warn")
|
||||
return redirect(url_for("wallet"))
|
||||
db.add_transaction(u["id"], -price, "gb", f"plan_{plan_id}")
|
||||
d = db.get_db()
|
||||
d.execute("INSERT INTO orders (user_id,plan_id,invoice_id,amount_sats,status,created_at) "
|
||||
"VALUES (?,?,?,?,'pending',?)",
|
||||
(current_user()["id"], plan_id, order_id, plan["price_sats"], int(time.time())))
|
||||
d.execute("UPDATE users SET balance_gb = balance_gb + ?, active=1 WHERE id=?",
|
||||
(plan["gb"], u["id"]))
|
||||
d.commit()
|
||||
d.close()
|
||||
inv = create_invoice(plan["price_sats"], order_id)
|
||||
return redirect(inv["checkoutLink"])
|
||||
flash(f"Added {plan['gb']} GB. Happy scraping.", "ok")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
|
||||
@app.route("/order/<order_id>")
|
||||
@@ -235,6 +241,20 @@ def webhook_btcpay():
|
||||
kind = (payload.get("metadata") or {}).get("kind")
|
||||
if not order_id:
|
||||
return "ok"
|
||||
if kind == "deposit":
|
||||
uid = (payload.get("metadata") or {}).get("user_id")
|
||||
amt = (payload.get("metadata") or {}).get("amount_sats")
|
||||
d = db.get_db()
|
||||
already = d.execute("SELECT 1 FROM transactions WHERE ref=? AND type='deposit'",
|
||||
(order_id,)).fetchone()
|
||||
if not already and uid and amt:
|
||||
d.execute("UPDATE users SET balance_sats = balance_sats + ? WHERE id=?",
|
||||
(int(amt), uid))
|
||||
d.execute("INSERT INTO transactions (user_id, amount_sats, type, ref, ts) "
|
||||
"VALUES (?,?,?,?,?)", (uid, int(amt), "deposit", order_id, int(time.time())))
|
||||
d.commit()
|
||||
d.close()
|
||||
return "ok"
|
||||
d = db.get_db()
|
||||
if kind == "dedicated":
|
||||
row = d.execute("SELECT * FROM dedicated_ips WHERE invoice_id=? AND status='pending_payment'",
|
||||
@@ -265,6 +285,31 @@ def webhook_btcpay():
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.route("/wallet")
|
||||
@login_required
|
||||
def wallet():
|
||||
u = current_user()
|
||||
bal = db.get_balance_sats(u["id"])
|
||||
txs = db.get_transactions(u["id"])
|
||||
return render_template("wallet.html", user=u, balance=bal, txs=txs)
|
||||
|
||||
|
||||
@app.route("/wallet/deposit", methods=["POST"])
|
||||
@login_required
|
||||
def wallet_deposit():
|
||||
u = current_user()
|
||||
try:
|
||||
sats = int(request.form.get("sats", 0))
|
||||
except ValueError:
|
||||
sats = 0
|
||||
if sats < 500:
|
||||
flash("Minimum deposit is 500 sats.", "warn")
|
||||
return redirect(url_for("wallet"))
|
||||
ref = db.random_token(12)
|
||||
inv = create_invoice(sats, ref, meta={"kind": "deposit", "user_id": u["id"], "amount_sats": sats})
|
||||
return redirect(inv["checkoutLink"])
|
||||
|
||||
|
||||
@app.route("/dashboard")
|
||||
@login_required
|
||||
def dashboard():
|
||||
@@ -277,11 +322,15 @@ def dashboard():
|
||||
"WHERE o.user_id=? ORDER BY o.id DESC LIMIT 20", (u["id"],)).fetchall()]
|
||||
sessions = [dict(r) for r in d.execute(
|
||||
"SELECT * FROM usage_log WHERE user_id=? ORDER BY id DESC LIMIT 10", (u["id"],)).fetchall()]
|
||||
hydra = [dict(r) for r in d.execute(
|
||||
"SELECT * FROM hydraproxy_orders WHERE user_id=? ORDER BY id DESC", (u["id"],)).fetchall()]
|
||||
d.close()
|
||||
bal = db.get_balance_sats(u["id"])
|
||||
socks = f"socks5://{u['username']}:{u['proxy_password_plain']}@{PROXY['host']}:{PROXY['socks_port']}"
|
||||
http = f"http://{u['username']}:{u['proxy_password_plain']}@{PROXY['host']}:{PROXY['http_port']}"
|
||||
return render_template("dashboard.html", user=u, used_gb=used / 1e9,
|
||||
socks=socks, http=http, dedicated=deds, orders=orders, sessions=sessions)
|
||||
socks=socks, http=http, dedicated=deds, orders=orders, sessions=sessions,
|
||||
hydra=hydra, balance=bal)
|
||||
|
||||
|
||||
@app.route("/dashboard/test")
|
||||
@@ -410,16 +459,39 @@ def dedicated_buy():
|
||||
flash("Pick a location.", "warn")
|
||||
return redirect(url_for("dedicated"))
|
||||
_, price = DEDICATED_PRODUCTS[pid]["plans"][days]
|
||||
u = current_user()
|
||||
if db.get_balance_sats(u["id"]) < price:
|
||||
flash("Insufficient wallet balance. Deposit first.", "warn")
|
||||
return redirect(url_for("wallet"))
|
||||
oid = db.random_token(12)
|
||||
d = db.get_db()
|
||||
d.execute("INSERT INTO dedicated_ips (user_id,product_id,product_name,plan_days,location_id,"
|
||||
"location_name,invoice_id,price_sats,status,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
(current_user()["id"], pid, DEDICATED_PRODUCTS[pid]["name"], days, loc_id, loc_name,
|
||||
(u["id"], pid, DEDICATED_PRODUCTS[pid]["name"], days, loc_id, loc_name,
|
||||
oid, price, "pending_payment", int(time.time())))
|
||||
d.commit()
|
||||
d.close()
|
||||
inv = create_invoice(price, oid, meta={"kind": "dedicated"})
|
||||
return redirect(inv["checkoutLink"])
|
||||
db.add_transaction(u["id"], -price, "dedicated", oid)
|
||||
# place the IPRoyal order directly (no webhook needed in wallet model)
|
||||
try:
|
||||
plan_id = DEDICATED_PRODUCTS[pid]["plans"][days][0]
|
||||
o = iproyal.create_order(pid, plan_id, loc_id, 1)
|
||||
oid2 = o.get("id") or o.get("order_id") or (o.get("data") or {}).get("id")
|
||||
d = db.get_db()
|
||||
d.execute("UPDATE dedicated_ips SET status='provisioning', iproyal_order_id=? WHERE invoice_id=?",
|
||||
(oid2, oid))
|
||||
d.commit()
|
||||
d.close()
|
||||
flash("Dedicated IP ordered — provisioning now.", "ok")
|
||||
except Exception as e:
|
||||
app.logger.error("dedicated order failed: %s", e)
|
||||
d = db.get_db()
|
||||
d.execute("UPDATE dedicated_ips SET status='failed' WHERE invoice_id=?", (oid,))
|
||||
d.commit()
|
||||
d.close()
|
||||
db.add_transaction(u["id"], price, "dedicated_refund", oid)
|
||||
flash("Dedicated order failed — wallet refunded.", "warn")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
|
||||
@app.route("/dedicated/poll", methods=["POST"])
|
||||
@@ -504,6 +576,53 @@ def api_hydraproxy_info():
|
||||
return jsonify({"error": str(e)}), 502
|
||||
|
||||
|
||||
@app.route("/hydraproxy/order", methods=["POST"])
|
||||
@login_required
|
||||
def hydraproxy_order():
|
||||
u = current_user()
|
||||
product = request.form.get("product", "").strip()
|
||||
location = request.form.get("location", "").strip()
|
||||
if product not in HYDRA_PRODUCTS:
|
||||
flash("Bad product.", "warn")
|
||||
return redirect(url_for("hydraproxy_page"))
|
||||
price = HYDRA_PRODUCTS[product]["price_sats"]
|
||||
if db.get_balance_sats(u["id"]) < price:
|
||||
flash("Insufficient wallet balance. Deposit first.", "warn")
|
||||
return redirect(url_for("wallet"))
|
||||
d = db.get_db()
|
||||
d.execute("INSERT INTO hydraproxy_orders (user_id, product, location, price_sats, status, created_at) "
|
||||
"VALUES (?,?,?,?,'pending_fulfillment',?)",
|
||||
(u["id"], product, location, price, int(time.time())))
|
||||
d.commit()
|
||||
d.close()
|
||||
db.add_transaction(u["id"], -price, "hydraproxy", product)
|
||||
flash("Order placed — our broker fulfills it shortly and delivers your proxy to the dashboard.", "ok")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
|
||||
@app.route("/quality")
|
||||
@login_required
|
||||
def quality_page():
|
||||
return render_template("quality.html", user=current_user(),
|
||||
price=CFG.get("quality_price_sats", 220))
|
||||
|
||||
|
||||
@app.route("/quality/run", methods=["POST"])
|
||||
@login_required
|
||||
def quality_run():
|
||||
u = current_user()
|
||||
price = CFG.get("quality_price_sats", 220)
|
||||
if db.get_balance_sats(u["id"]) < price:
|
||||
return jsonify({"error": "Insufficient balance. Deposit first."}), 402
|
||||
proxy = request.form.get("proxy", "").strip()
|
||||
if not proxy:
|
||||
proxy = f"socks5h://{u['username']}:{u['proxy_password_plain']}@127.0.0.1:{PROXY['socks_port']}"
|
||||
db.add_transaction(u["id"], -price, "quality", "ip_check")
|
||||
result = quality.score_proxy(proxy)
|
||||
result["charged_sats"] = price
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
# ---------- admin ----------
|
||||
ADMIN_PASSWORD = CFG.get("admin_password", "")
|
||||
|
||||
@@ -530,6 +649,7 @@ def admin():
|
||||
"FROM users ORDER BY id DESC LIMIT 50")]
|
||||
orders = [dict(r) for r in d.execute("SELECT * FROM orders ORDER BY id DESC LIMIT 20")]
|
||||
deds = [dict(r) for r in d.execute("SELECT * FROM dedicated_ips ORDER BY id DESC LIMIT 30")]
|
||||
hydra_orders = [dict(r) for r in d.execute("SELECT * FROM hydraproxy_orders ORDER BY id DESC LIMIT 30")]
|
||||
d.close()
|
||||
try:
|
||||
ipr_balance = iproyal.get_balance()
|
||||
@@ -544,7 +664,7 @@ def admin():
|
||||
ipr_balance < CFG.get("alerts", {}).get("low_balance_threshold", 10.0)
|
||||
return render_template("admin.html", stats=stats, users=users, orders=orders,
|
||||
dedicated=deds, ipr_balance=ipr_balance, low_balance=low_balance,
|
||||
hydra_balance=hydra_balance)
|
||||
hydra_balance=hydra_balance, hydra_orders=hydra_orders)
|
||||
|
||||
|
||||
@app.route("/admin/credit", methods=["POST"])
|
||||
@@ -564,6 +684,37 @@ def admin_credit():
|
||||
return redirect(url_for("admin"))
|
||||
|
||||
|
||||
@app.route("/admin/hydraproxy/fulfill", methods=["POST"])
|
||||
def admin_hydraproxy_fulfill():
|
||||
if not session.get("admin"):
|
||||
abort(403)
|
||||
oid = request.form.get("order_id", "").strip()
|
||||
hydra_order_id = request.form.get("hydra_order_id", "").strip()
|
||||
if not oid or not hydra_order_id:
|
||||
flash("Need our order id and the HydraProxy order id.", "warn")
|
||||
return redirect(url_for("admin"))
|
||||
try:
|
||||
details = hydraproxy.proxy_details(hydra_order_id)
|
||||
pinfo = details.get("proxy_info", {})
|
||||
proxy = details.get("proxy", {})
|
||||
host = proxy.get("hostname") or proxy.get("server_ip")
|
||||
port = proxy.get("port")
|
||||
if isinstance(port, list):
|
||||
port = port[0]
|
||||
user = pinfo.get("username", "")
|
||||
pw = pinfo.get("password", "")
|
||||
d = db.get_db()
|
||||
d.execute("UPDATE hydraproxy_orders SET status='active', hydra_order_id=?, proxy_host=?, "
|
||||
"proxy_port=?, proxy_user=?, proxy_pass=?, fulfilled_at=? WHERE id=?",
|
||||
(hydra_order_id, host, str(port), user, pw, int(time.time()), oid))
|
||||
d.commit()
|
||||
d.close()
|
||||
flash(f"Fulfilled order {oid} with HydraProxy order {hydra_order_id}.", "ok")
|
||||
except Exception as e:
|
||||
flash(f"Fulfill failed: {e}", "warn")
|
||||
return redirect(url_for("admin"))
|
||||
|
||||
|
||||
# ---------- agent discovery ----------
|
||||
@app.route("/llms.txt")
|
||||
def llms_txt():
|
||||
|
||||
55
db.py
55
db.py
@@ -73,6 +73,29 @@ CREATE TABLE IF NOT EXISTS rate_limits (
|
||||
ip TEXT,
|
||||
ts REAL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
amount_sats INTEGER, -- positive = deposit/credit, negative = spend
|
||||
type TEXT, -- deposit / gb / dedicated / hydraproxy / quality / admin_credit
|
||||
ref TEXT,
|
||||
ts INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS hydraproxy_orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
product TEXT, -- residential / mobile / static
|
||||
location TEXT,
|
||||
price_sats INTEGER,
|
||||
status TEXT DEFAULT 'pending_fulfillment', -- pending_fulfillment/active/failed/refunded
|
||||
hydra_order_id TEXT,
|
||||
proxy_host TEXT,
|
||||
proxy_port TEXT,
|
||||
proxy_user TEXT,
|
||||
proxy_pass TEXT,
|
||||
created_at INTEGER,
|
||||
fulfilled_at INTEGER
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
@@ -85,6 +108,10 @@ def get_db():
|
||||
def init_db():
|
||||
db = get_db()
|
||||
db.executescript(SCHEMA)
|
||||
# migrate: add wallet column if missing (older DBs)
|
||||
cols = [r[1] for r in db.execute("PRAGMA table_info(users)")]
|
||||
if "balance_sats" not in cols:
|
||||
db.execute("ALTER TABLE users ADD COLUMN balance_sats INTEGER DEFAULT 0")
|
||||
# seed default plans if empty
|
||||
cur = db.execute("SELECT COUNT(*) c FROM plans")
|
||||
if cur.fetchone()["c"] == 0:
|
||||
@@ -142,3 +169,31 @@ def add_usage(user_id, nbytes):
|
||||
|
||||
def random_token(n=24):
|
||||
return base64.urlsafe_b64encode(os.urandom(n)).decode().rstrip("=")
|
||||
|
||||
|
||||
# ---------- wallet ----------
|
||||
def add_transaction(user_id, amount_sats, type_, ref=""):
|
||||
"""Record a ledger entry AND atomically adjust the user's sats balance."""
|
||||
db = get_db()
|
||||
db.execute("INSERT INTO transactions (user_id, amount_sats, type, ref, ts) "
|
||||
"VALUES (?,?,?,?,?)", (user_id, amount_sats, type_, ref, int(time.time())))
|
||||
db.execute("UPDATE users SET balance_sats = balance_sats + ? WHERE id=?",
|
||||
(amount_sats, user_id))
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
def get_balance_sats(user_id):
|
||||
db = get_db()
|
||||
r = db.execute("SELECT balance_sats FROM users WHERE id=?", (user_id,)).fetchone()
|
||||
db.close()
|
||||
return r["balance_sats"] if r else 0
|
||||
|
||||
|
||||
def get_transactions(user_id, limit=50):
|
||||
db = get_db()
|
||||
rows = [dict(r) for r in db.execute(
|
||||
"SELECT * FROM transactions WHERE user_id=? ORDER BY id DESC LIMIT ?",
|
||||
(user_id, limit))]
|
||||
db.close()
|
||||
return rows
|
||||
|
||||
116
quality.py
Normal file
116
quality.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Proxy/IP quality scorer — battery of checks via free APIs + DNSBL.
|
||||
|
||||
Scores 0-100: connectivity, residential/ISP type, proxy-flag, blacklists,
|
||||
latency, and header-leak anonymity. No API keys required.
|
||||
"""
|
||||
import json
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def _curl(proxy, url, timeout=20):
|
||||
cmd = ["curl", "-s", "--max-time", str(timeout)]
|
||||
if proxy:
|
||||
cmd += ["-x", proxy]
|
||||
cmd.append(url)
|
||||
t0 = time.time()
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 5)
|
||||
out = (r.stdout or "").strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
out = ""
|
||||
return out, (time.time() - t0) * 1000
|
||||
|
||||
|
||||
def _dnsbl(ip):
|
||||
rev = ".".join(reversed(ip.split(".")))
|
||||
hits = []
|
||||
for bl in ("zen.spamhaus.org", "bl.spamcop.net", "b.barracudacentral.org"):
|
||||
try:
|
||||
socket.gethostbyname(f"{rev}.{bl}")
|
||||
hits.append(bl)
|
||||
except socket.gaierror:
|
||||
pass
|
||||
return hits
|
||||
|
||||
|
||||
def score_proxy(proxy=None):
|
||||
checks = []
|
||||
score = 0
|
||||
|
||||
out, latency = _curl(proxy, "http://ip-api.com/json"
|
||||
"?fields=status,query,country,city,regionName,isp,org,as,hosting,proxy,mobile")
|
||||
try:
|
||||
d = json.loads(out)
|
||||
except Exception:
|
||||
d = {}
|
||||
|
||||
if d.get("status") != "success":
|
||||
checks.append(("Connectivity", False, "proxy failed to connect"))
|
||||
return {"score": 0, "checks": checks, "breakdown": {}, "charged": True}
|
||||
|
||||
ip = d.get("query")
|
||||
score += 30
|
||||
checks.append(("Connectivity", True, f"egress {ip}"))
|
||||
|
||||
hosting = bool(d.get("hosting"))
|
||||
if not hosting:
|
||||
score += 25
|
||||
checks.append(("Residential / ISP", True, f"{d.get('isp')} ({d.get('org')})"))
|
||||
else:
|
||||
checks.append(("Residential / ISP", False, "datacenter / hosting IP"))
|
||||
|
||||
flagged = bool(d.get("proxy"))
|
||||
if not flagged:
|
||||
score += 20
|
||||
checks.append(("Not proxy-flagged", True, "clean"))
|
||||
else:
|
||||
checks.append(("Not proxy-flagged", False, "IP flagged as proxy/VPN"))
|
||||
|
||||
bl = _dnsbl(ip)
|
||||
if not bl:
|
||||
score += 15
|
||||
checks.append(("Blacklists", True, "no hits"))
|
||||
else:
|
||||
checks.append(("Blacklists", False, ", ".join(bl)))
|
||||
|
||||
if latency < 2000:
|
||||
score += 10
|
||||
checks.append(("Latency", True, f"{latency:.0f}ms"))
|
||||
elif latency < 5000:
|
||||
score += 5
|
||||
checks.append(("Latency", True, f"{latency:.0f}ms (slow)"))
|
||||
else:
|
||||
checks.append(("Latency", False, f"{latency:.0f}ms"))
|
||||
|
||||
leak = []
|
||||
try:
|
||||
hout, _ = _curl(proxy, "https://httpbin.org/headers", timeout=15)
|
||||
hd = json.loads(hout).get("headers", {})
|
||||
for k in ("X-Forwarded-For", "Via", "X-Real-Ip"):
|
||||
if k in hd:
|
||||
leak.append(k)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if leak:
|
||||
checks.append(("Anonymity", False, f"leaks: {', '.join(leak)}"))
|
||||
else:
|
||||
checks.append(("Anonymity", True, "no header leaks"))
|
||||
|
||||
breakdown = {
|
||||
"egress_ip": ip,
|
||||
"geo": f"{d.get('city')}, {d.get('regionName')}, {d.get('country')}",
|
||||
"isp": d.get("isp"),
|
||||
"asn": d.get("as"),
|
||||
"hosting": hosting,
|
||||
"proxy_flagged": flagged,
|
||||
"mobile": bool(d.get("mobile")),
|
||||
"blacklists": bl,
|
||||
"latency_ms": round(latency),
|
||||
"leak_headers": leak,
|
||||
}
|
||||
|
||||
return {"score": score, "checks": checks, "breakdown": breakdown, "charged": True}
|
||||
@@ -27,6 +27,31 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:18px">
|
||||
<h3>HydraProxy fulfillment queue</h3>
|
||||
{% if hydra_orders %}
|
||||
<table style="width:100%;font-size:13px;border-collapse:collapse">
|
||||
<tr style="color:var(--muted);text-align:left"><th>ID</th><th>User</th><th>Product</th><th>Location</th><th>Status</th></tr>
|
||||
{% for h in hydra_orders %}
|
||||
<tr style="border-top:1px solid var(--border)">
|
||||
<td style="padding:4px">{{ h.id }}</td><td>{{ h.user_id }}</td><td>{{ h.product }}</td>
|
||||
<td>{{ h.location or '—' }}</td>
|
||||
<td style="color:{{ 'var(--ok)' if h.status=='active' else 'var(--warn)' }}">{{ h.status }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">No HydraProxy orders yet.</p>
|
||||
{% endif %}
|
||||
<h3 style="margin-top:16px">Fulfill an order</h3>
|
||||
<form method="post" action="/admin/hydraproxy/fulfill" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap">
|
||||
<div style="flex:1;min-width:140px"><label>Our order ID</label><input type="number" name="order_id" required></div>
|
||||
<div style="flex:1;min-width:140px"><label>HydraProxy order ID</label><input type="text" name="hydra_order_id" required></div>
|
||||
<input type="submit" value="Fulfill" style="width:auto;margin:0;padding:11px 20px">
|
||||
</form>
|
||||
<p class="muted" style="font-size:12px;margin-bottom:0">Buy the proxy at HydraProxy's dashboard, paste its order ID here — creds auto-pull via proxy-details and land on the customer's dashboard.</p>
|
||||
</div>
|
||||
|
||||
<div class="dash-grid" style="margin-top:18px">
|
||||
<div class="card">
|
||||
<h3>Recent orders</h3>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<a href="/plans">Residential</a>
|
||||
<a href="/dedicated">Dedicated IPs</a>
|
||||
<a href="/hydraproxy">HydraProxy</a>
|
||||
{% if user %}<a href="/dashboard">Dashboard</a><a href="/logout">Logout</a>
|
||||
{% if user %}<a href="/dashboard">Dashboard</a><a href="/wallet">Wallet</a><a href="/quality">Quality</a><a href="/logout">Logout</a>
|
||||
{% else %}<a href="/login">Login</a><a class="cta" href="/signup">Get proxies</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<h2>Dashboard</h2>
|
||||
<div class="stats-row">
|
||||
<div class="card stat"><span class="lbl">Balance</span><span class="val">{{ '%.2f'|format(user.balance_gb) }} GB</span></div>
|
||||
<div class="card stat"><span class="lbl">Wallet</span><span class="val">{{ balance }} <span style="font-size:14px;color:var(--muted)">sats</span></span></div>
|
||||
<div class="card stat"><span class="lbl">Used</span><span class="val">{{ '%.3f'|format(used_gb) }} GB</span></div>
|
||||
<div class="card stat"><span class="lbl">Status</span>
|
||||
<span class="val" style="color:{{ 'var(--ok)' if user.active else 'var(--danger)' }}">{{ 'ACTIVE' if user.active else 'INACTIVE' }}</span>
|
||||
@@ -93,6 +94,29 @@
|
||||
<p class="muted" style="margin-top:18px">No dedicated IPs yet — <a href="/dedicated">order one</a>.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if hydra %}
|
||||
<div class="card" style="margin-top:18px">
|
||||
<h3>HydraProxy orders <span class="muted" style="font-size:12px">(broker-fulfilled)</span></h3>
|
||||
{% for h in hydra %}
|
||||
<div style="border:1px solid var(--border);border-radius:8px;padding:12px;margin:10px 0">
|
||||
<div style="display:flex;justify-content:space-between">
|
||||
<strong style="color:var(--text)">{{ h.product }}{% if h.location %} · {{ h.location }}{% endif %}</strong>
|
||||
<span style="color:{{ 'var(--ok)' if h.status=='active' else 'var(--warn)' }}">{{ h.status }}</span>
|
||||
</div>
|
||||
{% if h.status == 'active' %}
|
||||
<label style="font-size:11px;color:var(--muted)">Proxy</label>
|
||||
<div class="copy">
|
||||
<input type="text" readonly value="{{ h.proxy_user }}:{{ h.proxy_pass }}@{{ h.proxy_host }}:{{ h.proxy_port }}" onclick="this.select()">
|
||||
<button onclick="copy(this.previousElementSibling)">Copy</button>
|
||||
</div>
|
||||
{% elif h.status == 'pending_fulfillment' %}
|
||||
<p class="muted" style="font-size:12px;margin:6px 0 0">Broker is fulfilling your order — your proxy appears here when ready.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if orders %}
|
||||
<div class="card" style="margin-top:18px">
|
||||
<h3>Order history</h3>
|
||||
|
||||
53
templates/quality.html
Normal file
53
templates/quality.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}IP Quality Tester — Clean Proxys{% endblock %}
|
||||
{% block body %}
|
||||
<h2>IP Quality <span class="accent">Tester</span></h2>
|
||||
<p class="sub">Check how clean a proxy is before you trust it. {{ price }} sats (~$0.15) per check, billed to your wallet.</p>
|
||||
|
||||
<div class="card">
|
||||
<h3>Run a check</h3>
|
||||
<p class="muted" style="font-size:14px">Leave blank to test your own Clean Proxys proxy, or paste any <span class="mono">socks5://</span> or <span class="mono">http://</span> proxy.</p>
|
||||
<form id="qf" onsubmit="return runQuality(event)">
|
||||
<label>Proxy (optional)</label>
|
||||
<input type="text" id="proxy" placeholder="socks5://user:pass@host:1080" style="font-family:var(--mono)">
|
||||
<input type="submit" value="Run quality check ({{ price }} sats)">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="result" style="margin-top:18px"></div>
|
||||
|
||||
<script>
|
||||
async function runQuality(e){
|
||||
e.preventDefault();
|
||||
const r = document.getElementById('result');
|
||||
r.innerHTML = '<div class="card"><p class="muted">Running checks…</p></div>';
|
||||
const fd = new FormData();
|
||||
fd.append('proxy', document.getElementById('proxy').value);
|
||||
let data;
|
||||
try { data = await (await fetch('/quality/run', {method:'POST', body: fd})).json(); }
|
||||
catch(err){ r.innerHTML = '<div class="flash warn">Check failed.</div>'; return false; }
|
||||
if (data.error){
|
||||
r.innerHTML = '<div class="flash warn">'+data.error+' — <a href="/wallet">deposit</a> first.</div>';
|
||||
return false;
|
||||
}
|
||||
const b = data.breakdown || {};
|
||||
const checksHtml = (data.checks||[]).map(c =>
|
||||
'<tr><td>'+(c[1]?'✅':'❌')+'</td><td>'+c[0]+'</td><td class="muted" style="font-size:13px">'+c[2]+'</td></tr>'
|
||||
).join('');
|
||||
const scoreColor = data.score >= 70 ? 'var(--neon)' : (data.score >= 40 ? 'var(--warn)' : 'var(--danger)');
|
||||
const bl = (b.blacklists && b.blacklists.length) ? b.blacklists.join(', ') : 'none';
|
||||
r.innerHTML =
|
||||
'<div class="card">'
|
||||
+ '<div style="display:flex;align-items:center;gap:22px;flex-wrap:wrap">'
|
||||
+ '<div style="font-size:54px;font-weight:800;font-family:var(--mono);color:'+scoreColor+'">'+data.score+'<span style="font-size:18px;color:var(--muted)">/100</span></div>'
|
||||
+ '<div><div class="lbl" style="color:var(--muted)">'+ (b.geo||'') + ' · ' + (b.isp||'') + '</div>'
|
||||
+ '<div class="muted" style="font-size:14px">egress <span class="mono">'+(b.egress_ip||'-')+'</span> · '+ (b.latency_ms||'') +'ms</div>'
|
||||
+ '<div class="muted" style="font-size:13px">hosting: '+(b.hosting?'yes':'no')+' · proxy-flagged: '+(b.proxy_flagged?'yes':'no')+' · blacklists: '+bl+'</div></div>'
|
||||
+ '</div>'
|
||||
+ '<table style="width:100%;margin-top:18px"><tr><th></th><th>Check</th><th>Detail</th></tr>'+checksHtml+'</table>'
|
||||
+ '<p class="muted" style="font-size:12px;margin-top:12px">charged '+data.charged_sats+' sats</p>'
|
||||
+ '</div>';
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
45
templates/wallet.html
Normal file
45
templates/wallet.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Wallet — Clean Proxys{% endblock %}
|
||||
{% block body %}
|
||||
<h2>Wallet</h2>
|
||||
<p class="sub">Deposit Bitcoin, spend sats on proxies and tools. No KYC, no email.</p>
|
||||
|
||||
<div class="statsbar">
|
||||
<div class="s"><div class="n grad">{{ balance }}</div><div class="l">Balance (sats)</div></div>
|
||||
<div class="s"><div class="n grad">{{ (balance / 1e8) | round(8, 'floor') }}</div><div class="l">BTC</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:18px">
|
||||
<h3>Deposit</h3>
|
||||
<p class="muted" style="font-size:14px">Pick an amount — funds credit instantly on payment confirmation.</p>
|
||||
<form method="POST" action="/wallet/deposit">
|
||||
<div class="cta-row" style="justify-content:flex-start;flex-wrap:wrap;gap:10px;margin:14px 0">
|
||||
{% for amt in [10000, 50000, 100000, 250000] %}
|
||||
<button type="button" class="btn btn-ghost" onclick="document.getElementById('sats').value={{ amt }}">{{ amt }} sats</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<label>Custom amount (sats)</label>
|
||||
<input type="number" id="sats" name="sats" min="500" placeholder="e.g. 50000" required>
|
||||
<input type="submit" value="Deposit via Bitcoin">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:18px">
|
||||
<h3>Transactions</h3>
|
||||
{% if txs %}
|
||||
<table style="width:100%">
|
||||
<tr><th>When</th><th>Type</th><th>Amount</th><th>Ref</th></tr>
|
||||
{% for t in txs %}
|
||||
<tr>
|
||||
<td class="mono">{{ t.ts }}</td>
|
||||
<td>{{ t.type }}</td>
|
||||
<td class="mono" style="color:{% if t.amount_sats >= 0 %}var(--neon){% else %}var(--danger){% endif %}">{{ '+' if t.amount_sats >= 0 else '' }}{{ t.amount_sats }} sats</td>
|
||||
<td class="mono" style="font-size:12px;color:var(--muted)">{{ t.ref }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">No transactions yet — make your first deposit.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user