mirror of
https://github.com/andrey271192/kaskad_web_vpn.git
synced 2026-09-21 12:01:59 +00:00
feat: Kaskad Web UI v2 dashboard
System/services/NAT CRUD, iptables chain KASKAD_WEB, host-network Docker install, docker CLI in image for status row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
120
app.py
120
app.py
@@ -1,10 +1,24 @@
|
||||
"""Мини-сайт про каскад AmneziaWG: только страница и HTTP Basic Auth."""
|
||||
"""Kaskad Web UI v2 — Flask + Basic Auth + NAT CRUD."""
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
|
||||
from flask import Flask, Response, render_template, request
|
||||
from flask import Flask, Response, jsonify, render_template, request
|
||||
|
||||
from kaskad_store import (
|
||||
iptables_chain_dump,
|
||||
load_rules,
|
||||
normalize_rule,
|
||||
save_rules,
|
||||
startup_resync,
|
||||
sync_iptables,
|
||||
)
|
||||
from system_info import collect_services_rows, collect_system_payload
|
||||
|
||||
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
USER = os.environ.get("BASIC_AUTH_USER", "user1").strip() or "user1"
|
||||
PW = os.environ.get("BASIC_AUTH_PASSWORD", "").strip()
|
||||
@@ -44,7 +58,107 @@ def health():
|
||||
@app.get("/")
|
||||
@requires_auth
|
||||
def index():
|
||||
return render_template("index.html", panel_url=PANEL_URL)
|
||||
title = os.environ.get("KASKAD_UI_TITLE", "Kaskad Web UI v2").strip()
|
||||
return render_template("index.html", panel_url=PANEL_URL, ui_title=title)
|
||||
|
||||
|
||||
def _need_auth_json(view):
|
||||
@functools.wraps(view)
|
||||
def wrapped(*args, **kwargs):
|
||||
auth = request.authorization
|
||||
if auth is None or not check_auth(auth.username or "", auth.password or ""):
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
@app.get("/api/system")
|
||||
@_need_auth_json
|
||||
def api_system():
|
||||
return jsonify(collect_system_payload())
|
||||
|
||||
|
||||
@app.get("/api/services")
|
||||
@_need_auth_json
|
||||
def api_services():
|
||||
return jsonify({"services": collect_services_rows()})
|
||||
|
||||
|
||||
@app.get("/api/clients")
|
||||
@_need_auth_json
|
||||
def api_clients_list():
|
||||
return jsonify({"clients": load_rules()})
|
||||
|
||||
|
||||
@app.post("/api/clients")
|
||||
@_need_auth_json
|
||||
def api_clients_add():
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
try:
|
||||
rule = normalize_rule(body)
|
||||
rules = load_rules()
|
||||
rules.append(rule)
|
||||
ok, msg = sync_iptables(rules)
|
||||
if not ok:
|
||||
return jsonify({"error": msg}), 500
|
||||
save_rules(rules)
|
||||
return jsonify({"client": rule}), 201
|
||||
except (KeyError, TypeError, ValueError) as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@app.put("/api/clients/<cid>")
|
||||
@_need_auth_json
|
||||
def api_clients_update(cid: str):
|
||||
body = request.get_json(force=True, silent=True) or {}
|
||||
rules = load_rules()
|
||||
idx = next((i for i, r in enumerate(rules) if str(r.get("id")) == cid), None)
|
||||
if idx is None:
|
||||
return jsonify({"error": "не найдено"}), 404
|
||||
try:
|
||||
rule = normalize_rule(body, rid=cid)
|
||||
rules[idx] = rule
|
||||
ok, msg = sync_iptables(rules)
|
||||
if not ok:
|
||||
return jsonify({"error": msg}), 500
|
||||
save_rules(rules)
|
||||
return jsonify({"client": rule})
|
||||
except (KeyError, TypeError, ValueError) as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@app.delete("/api/clients/<cid>")
|
||||
@_need_auth_json
|
||||
def api_clients_delete(cid: str):
|
||||
rules = load_rules()
|
||||
new_rules = [r for r in rules if str(r.get("id")) != cid]
|
||||
if len(new_rules) == len(rules):
|
||||
return jsonify({"error": "не найдено"}), 404
|
||||
ok, msg = sync_iptables(new_rules)
|
||||
if not ok:
|
||||
return jsonify({"error": msg}), 500
|
||||
save_rules(new_rules)
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.post("/api/iptables/sync")
|
||||
@_need_auth_json
|
||||
def api_iptables_sync():
|
||||
rules = load_rules()
|
||||
ok, msg = sync_iptables(rules)
|
||||
if not ok:
|
||||
return jsonify({"error": msg}), 500
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.get("/api/iptables/raw")
|
||||
@_need_auth_json
|
||||
def api_iptables_raw():
|
||||
return jsonify({"raw": iptables_chain_dump()})
|
||||
|
||||
|
||||
startup_resync()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
Reference in New Issue
Block a user