diff --git a/admin.py b/admin.py index 9ce6d17..6d898aa 100644 --- a/admin.py +++ b/admin.py @@ -32,7 +32,13 @@ def set_country(username, country): print(f"OK {username} country={country}") +ALLOWED_FIELDS = {"country", "city", "region", "sticky", "active", "balance_gb"} + + def set_field(username, field, value): + if field not in ALLOWED_FIELDS: + print(f"ERROR: field '{field}' not allowed (whitelist: {sorted(ALLOWED_FIELDS)})") + return d = db.get_db() d.execute(f"UPDATE users SET {field}=? WHERE username=?", (value, username)) d.commit() diff --git a/app.py b/app.py index 2a7fc59..52b30c9 100644 --- a/app.py +++ b/app.py @@ -17,6 +17,8 @@ import iproyal 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) # simple SQLite-backed rate limiter (shared across gunicorn workers) def _client_ip(): @@ -129,6 +131,9 @@ def index(): @app.route("/signup", methods=["GET", "POST"]) def signup(): if request.method == "POST": + if not _rate_ok(_client_ip(), limit=20, window=300): + flash("Too many attempts. Slow down.", "warn") + return render_template("signup.html") username = request.form.get("username", "").strip() password = request.form.get("password", "") if len(username) < 3 or len(password) < 6: @@ -158,6 +163,9 @@ def signup(): @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": + if not _rate_ok(_client_ip(), limit=20, window=300): + flash("Too many attempts. Slow down.", "warn") + return render_template("login.html") username = request.form.get("username", "").strip() password = request.form.get("password", "") d = db.get_db() @@ -377,6 +385,16 @@ def dedicated_locations(): return jsonify({"error": str(e)}), 502 +@app.route("/api/v1/dedicated/locations") +def api_dedicated_locations(): + pid = request.args.get("product", 9, type=int) + try: + return jsonify({"locations": [{"id": i, "name": n, "in_stock": s} + for i, n, s in iproyal.list_locations(pid)]}) + except Exception as e: + return jsonify({"error": str(e)}), 502 + + @app.route("/dedicated/buy", methods=["POST"]) @login_required def dedicated_buy(): @@ -405,7 +423,9 @@ def dedicated_buy(): @app.route("/dedicated/poll", methods=["POST"]) def dedicated_poll(): - """Admin/agent hook: poll provisioning for pending dedicated orders.""" + """Admin hook: poll provisioning for pending dedicated orders.""" + if not session.get("admin"): + abort(403) ok, failed = poll_provisioning() return jsonify({"provisioned": ok, "failed": failed}) @@ -457,7 +477,7 @@ def poll_provisioning(): # ---------- admin ---------- -ADMIN_PASSWORD = CFG.get("admin_password", "czapiewski") +ADMIN_PASSWORD = CFG.get("admin_password", "") @app.route("/admin", methods=["GET", "POST"])