diff --git a/app.py b/app.py index fd4c7f2..58b12a9 100644 --- a/app.py +++ b/app.py @@ -18,6 +18,25 @@ CFG = db.CFG app = Flask(__name__) 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"] PROXY = CFG["proxy_public"] SITE_HOST = CFG.get("site_host", "clean-proxys.thetempleofdoom.com") @@ -229,11 +248,33 @@ def dashboard(): d = db.get_db() 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()] + 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() 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) + 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"]) @@ -490,6 +531,8 @@ def api_plans(): @app.route("/api/v1/proxy", methods=["POST"]) def api_proxy(): """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 {} username = (data.get("username") or "").strip() password = data.get("password") or "" diff --git a/db.py b/db.py index 07afab5..6b450ec 100644 --- a/db.py +++ b/db.py @@ -69,6 +69,10 @@ CREATE TABLE IF NOT EXISTS dedicated_ips ( created_at INTEGER, expires_at INTEGER ); +CREATE TABLE IF NOT EXISTS rate_limits ( + ip TEXT, + ts REAL +); """ diff --git a/templates/dashboard.html b/templates/dashboard.html index 4414519..eea3a64 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -28,6 +28,8 @@
Proxy password is unique to you. Use it for proxy auth, not your web password.
+ + @@ -84,7 +86,43 @@No dedicated IPs yet — order one.
{% endif %} +{% if orders %} +| Plan | GB | sats | Status |
|---|---|---|---|
| {{ o.plan_name or '—' }} | +{{ o.plan_gb or '—' }} | +{{ o.amount_sats }} | +{{ o.status }} | +