This commit is contained in:
dekun
2026-05-30 11:52:21 +08:00
parent fd5e333daf
commit 4cd5a48dc1
9 changed files with 298 additions and 16 deletions
+90 -2
View File
@@ -1,9 +1,13 @@
import json
import os
import secrets
import urllib.error
import urllib.request
from pathlib import Path
from typing import Optional
from urllib.parse import urlencode
from flask import Flask, flash, redirect, render_template, request, url_for
from flask import Flask, flash, jsonify, redirect, render_template, request, url_for
from flask_login import LoginManager, current_user, login_required, login_user, logout_user
from flask_wtf.csrf import CSRFProtect
from werkzeug.middleware.proxy_fix import ProxyFix
@@ -159,7 +163,79 @@ def create_app() -> Flask:
.all()
)
grouped.append((g, svcs))
return render_template("index.html", grouped=grouped)
return render_template(
"index.html",
grouped=grouped,
hub_auto_login=os.environ.get("NAV_HUB_AUTO_LOGIN", "").strip() == "1",
)
@app.route("/api/embed/hub-login", methods=["POST"])
@csrf.exempt
@login_required
def api_embed_hub_login():
"""
本地导航代登录云端中控:服务端请求 hub /api/auth/login,返回 embed-auth URL。
避免浏览器在跨站 iframe 里丢弃 Set-Cookie。
"""
body = request.get_json(silent=True) or {}
sid = body.get("service_id")
base = (body.get("base_url") or "").strip().rstrip("/")
next_path = (body.get("next") or "/monitor").strip() or "/monitor"
if not next_path.startswith("/"):
next_path = "/" + next_path
username = (body.get("username") or os.environ.get("NAV_HUB_USERNAME") or "").strip()
password = body.get("password")
if password is None:
password = os.environ.get("NAV_HUB_PASSWORD") or ""
password = str(password)
if sid:
svc = db.session.get(Service, int(sid))
if not svc or not svc.is_hub_embed():
return jsonify({"ok": False, "detail": "服务不存在或未标记为中控"}), 400
base = svc.build_origin()
if not next_path or next_path == "/monitor":
p = (svc.path or "/monitor").strip() or "/monitor"
next_path = p if p.startswith("/") else "/" + p
if not base:
return jsonify({"ok": False, "detail": "缺少 base_url"}), 400
if not username or not password:
return jsonify({"ok": False, "detail": "缺少中控用户名或密码(可配置 NAV_HUB_USERNAME / NAV_HUB_PASSWORD"}), 400
payload = json.dumps({"username": username, "password": password}).encode("utf-8")
req = urllib.request.Request(
f"{base}/api/auth/login",
data=payload,
headers={"Content-Type": "application/json", "Accept": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as e:
try:
err_body = e.read().decode("utf-8", errors="replace")
detail = json.loads(err_body).get("detail", err_body)
except Exception:
detail = str(e)
return jsonify({"ok": False, "detail": detail or "中控登录失败"}), 401
except Exception as e:
return jsonify({"ok": False, "detail": f"无法连接中控: {e}"}), 502
try:
data = json.loads(raw)
except json.JSONDecodeError:
return jsonify({"ok": False, "detail": "中控返回非 JSON"}), 502
if not data.get("ok"):
return jsonify({"ok": False, "detail": data.get("detail") or "登录失败"}), 401
token = data.get("session_token")
if not token:
return jsonify({"ok": False, "detail": "中控未返回 session_token,请升级云端 hub 后重试"}), 502
q = urlencode({"token": token, "next": next_path})
embed_url = f"{base}/embed-auth?{q}"
return jsonify({"ok": True, "embed_auth_url": embed_url, "next": next_path})
# ---------- 分组管理 ----------
@app.route("/admin/groups")
@@ -261,6 +337,7 @@ def create_app() -> Flask:
path=path,
sort_order=form.sort_order.data or 0,
group_id=form.group_id.data,
embed_kind=(form.embed_kind.data or "").strip(),
)
db.session.add(s)
db.session.commit()
@@ -296,6 +373,7 @@ def create_app() -> Flask:
s.path = (form.path.data or "").strip() or "/"
s.sort_order = form.sort_order.data or 0
s.group_id = form.group_id.data
s.embed_kind = (form.embed_kind.data or "").strip()
db.session.commit()
flash("服务已更新", "success")
return redirect(url_for("admin_services"))
@@ -352,6 +430,16 @@ def _migrate_schema() -> None:
)
)
print("[nav] 已为 services 表添加 scheme 列(默认 http)。", flush=True)
cols = {c["name"] for c in insp.get_columns("services")}
if "embed_kind" not in cols:
with db.engine.begin() as conn:
conn.execute(
text(
"ALTER TABLE services ADD COLUMN embed_kind VARCHAR(16) "
"NOT NULL DEFAULT ''"
)
)
print("[nav] 已为 services 表添加 embed_kind 列。", flush=True)
except Exception as exc:
print(f"[nav] 数据库结构迁移跳过或失败: {exc}", flush=True)