Add order history, live proxy test, SQLite rate limiter (cross-worker + real client IP)
This commit is contained in:
45
app.py
45
app.py
@@ -18,6 +18,25 @@ CFG = db.CFG
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = CFG["secret_key"]
|
app.secret_key = CFG["secret_key"]
|
||||||
|
|
||||||
|
# simple SQLite-backed rate limiter (shared across gunicorn workers)
|
||||||
|
def _client_ip():
|
||||||
|
xff = request.headers.get("X-Forwarded-For", "")
|
||||||
|
return xff.split(",")[0].strip() if xff else (request.remote_addr or "?")
|
||||||
|
|
||||||
|
|
||||||
|
def _rate_ok(ip, limit=10, window=60):
|
||||||
|
now = time.time()
|
||||||
|
d = db.get_db()
|
||||||
|
d.execute("DELETE FROM rate_limits WHERE ts < ?", (now - window,))
|
||||||
|
count = d.execute("SELECT COUNT(*) c FROM rate_limits WHERE ip=?", (ip,)).fetchone()["c"]
|
||||||
|
if count >= limit:
|
||||||
|
d.close()
|
||||||
|
return False
|
||||||
|
d.execute("INSERT INTO rate_limits (ip, ts) VALUES (?,?)", (ip, now))
|
||||||
|
d.commit()
|
||||||
|
d.close()
|
||||||
|
return True
|
||||||
|
|
||||||
BTCPAY = CFG["btcpay"]
|
BTCPAY = CFG["btcpay"]
|
||||||
PROXY = CFG["proxy_public"]
|
PROXY = CFG["proxy_public"]
|
||||||
SITE_HOST = CFG.get("site_host", "clean-proxys.thetempleofdoom.com")
|
SITE_HOST = CFG.get("site_host", "clean-proxys.thetempleofdoom.com")
|
||||||
@@ -229,11 +248,33 @@ def dashboard():
|
|||||||
d = db.get_db()
|
d = db.get_db()
|
||||||
used = d.execute("SELECT COALESCE(SUM(bytes),0) s FROM usage_log WHERE user_id=?", (u["id"],)).fetchone()["s"]
|
used = d.execute("SELECT COALESCE(SUM(bytes),0) s FROM usage_log WHERE user_id=?", (u["id"],)).fetchone()["s"]
|
||||||
deds = [dict(r) for r in d.execute("SELECT * FROM dedicated_ips WHERE user_id=? ORDER BY id DESC", (u["id"],)).fetchall()]
|
deds = [dict(r) for r in d.execute("SELECT * FROM dedicated_ips WHERE user_id=? ORDER BY id DESC", (u["id"],)).fetchall()]
|
||||||
|
orders = [dict(r) for r in d.execute(
|
||||||
|
"SELECT o.*, p.name plan_name, p.gb plan_gb FROM orders o LEFT JOIN plans p ON o.plan_id=p.id "
|
||||||
|
"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()]
|
||||||
d.close()
|
d.close()
|
||||||
socks = f"socks5://{u['username']}:{u['proxy_password_plain']}@{PROXY['host']}:{PROXY['socks_port']}"
|
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']}"
|
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,
|
return render_template("dashboard.html", user=u, used_gb=used / 1e9,
|
||||||
socks=socks, http=http, dedicated=deds)
|
socks=socks, http=http, dedicated=deds, orders=orders, sessions=sessions)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/dashboard/test")
|
||||||
|
@login_required
|
||||||
|
def dashboard_test():
|
||||||
|
"""Live proxy check: egress through the customer's own creds via the gateway."""
|
||||||
|
u = current_user()
|
||||||
|
proxy = f"socks5h://{u['username']}:{u['proxy_password_plain']}@127.0.0.1:{PROXY['socks_port']}"
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
r = subprocess.run(["curl", "-s", "--max-time", "20", "-x", proxy,
|
||||||
|
"http://ip-api.com/json?fields=query,country,city,isp,status"],
|
||||||
|
capture_output=True, text=True, timeout=25)
|
||||||
|
return app.response_class(r.stdout or '{"status":"fail","message":"no response"}',
|
||||||
|
mimetype="application/json")
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"status": "fail", "message": str(e)}), 502
|
||||||
|
|
||||||
|
|
||||||
@app.route("/dashboard/location", methods=["POST"])
|
@app.route("/dashboard/location", methods=["POST"])
|
||||||
@@ -490,6 +531,8 @@ def api_plans():
|
|||||||
@app.route("/api/v1/proxy", methods=["POST"])
|
@app.route("/api/v1/proxy", methods=["POST"])
|
||||||
def api_proxy():
|
def api_proxy():
|
||||||
"""Agent-facing one-call purchase: create account + invoice, return creds + checkout URL."""
|
"""Agent-facing one-call purchase: create account + invoice, return creds + checkout URL."""
|
||||||
|
if not _rate_ok(_client_ip(), limit=10, window=60):
|
||||||
|
return jsonify({"error": "rate limited"}), 429
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
username = (data.get("username") or "").strip()
|
username = (data.get("username") or "").strip()
|
||||||
password = data.get("password") or ""
|
password = data.get("password") or ""
|
||||||
|
|||||||
4
db.py
4
db.py
@@ -69,6 +69,10 @@ CREATE TABLE IF NOT EXISTS dedicated_ips (
|
|||||||
created_at INTEGER,
|
created_at INTEGER,
|
||||||
expires_at INTEGER
|
expires_at INTEGER
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS rate_limits (
|
||||||
|
ip TEXT,
|
||||||
|
ts REAL
|
||||||
|
);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
<button onclick="copy(this.previousElementSibling)">Copy</button>
|
<button onclick="copy(this.previousElementSibling)">Copy</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted" style="font-size:12px;margin-bottom:0">Proxy password is unique to you. Use it for proxy auth, not your web password.</p>
|
<p class="muted" style="font-size:12px;margin-bottom:0">Proxy password is unique to you. Use it for proxy auth, not your web password.</p>
|
||||||
|
<button class="btn btn-ghost" id="testbtn" onclick="testProxy()" style="margin-top:12px">⚡ Test my proxy</button>
|
||||||
|
<pre class="code" id="testout" style="display:none;margin-top:10px"></pre>
|
||||||
<form method="post" action="/dashboard/rotate" style="margin-top:14px">
|
<form method="post" action="/dashboard/rotate" style="margin-top:14px">
|
||||||
<input type="submit" value="Rotate proxy password">
|
<input type="submit" value="Rotate proxy password">
|
||||||
</form>
|
</form>
|
||||||
@@ -84,7 +86,43 @@
|
|||||||
<p class="muted" style="margin-top:18px">No dedicated IPs yet — <a href="/dedicated">order one</a>.</p>
|
<p class="muted" style="margin-top:18px">No dedicated IPs yet — <a href="/dedicated">order one</a>.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if orders %}
|
||||||
|
<div class="card" style="margin-top:18px">
|
||||||
|
<h3>Order history</h3>
|
||||||
|
<table style="width:100%;font-size:13px;border-collapse:collapse">
|
||||||
|
<tr style="color:var(--muted);text-align:left"><th>Plan</th><th>GB</th><th>sats</th><th>Status</th></tr>
|
||||||
|
{% for o in orders %}
|
||||||
|
<tr style="border-top:1px solid var(--border)">
|
||||||
|
<td style="padding:4px">{{ o.plan_name or '—' }}</td>
|
||||||
|
<td>{{ o.plan_gb or '—' }}</td>
|
||||||
|
<td>{{ o.amount_sats }}</td>
|
||||||
|
<td style="color:{{ 'var(--ok)' if o.status=='paid' else 'var(--warn)' }}">{{ o.status }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function copy(el){ el.select(); document.execCommand('copy'); }
|
function copy(el){ el.select(); document.execCommand('copy'); }
|
||||||
|
async function testProxy(){
|
||||||
|
const btn = document.getElementById('testbtn');
|
||||||
|
const out = document.getElementById('testout');
|
||||||
|
btn.textContent = 'Testing…';
|
||||||
|
out.style.display = 'block';
|
||||||
|
out.textContent = '…';
|
||||||
|
try {
|
||||||
|
const r = await fetch('/dashboard/test');
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.status === 'success') {
|
||||||
|
out.textContent = 'egress ' + j.query + '\n' + j.country + ' · ' + j.city + ' · ' + j.isp;
|
||||||
|
} else {
|
||||||
|
out.textContent = 'proxy error: ' + (j.message || j.status);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
out.textContent = 'error: ' + e;
|
||||||
|
}
|
||||||
|
btn.textContent = '⚡ Test my proxy';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user