修复
This commit is contained in:
@@ -58,3 +58,7 @@
|
||||
# NAV_GATE_EXECUTOR_HOST=exec.你的域名
|
||||
# NAV_GATE_SCOUT_PORT=443
|
||||
# NAV_GATE_EXECUTOR_PORT=443
|
||||
# iframe 内自动代登录(须与云端 gate_scout PM2 的 NAV_EMBED_SESSION=1 配合)
|
||||
# NAV_GATE_SCOUT_USERNAME=admin
|
||||
# NAV_GATE_SCOUT_PASSWORD=你的扫单密码
|
||||
# NAV_GATE_SCOUT_AUTO_LOGIN=1
|
||||
|
||||
@@ -190,6 +190,85 @@ def _resolve_hub_embed_service(body: dict) -> tuple[str | None, str | None, str
|
||||
return base, next_path, None
|
||||
|
||||
|
||||
def _resolve_gate_scout_embed_service(
|
||||
body: dict,
|
||||
) -> tuple[str | None, str | None, str | None, str | None]:
|
||||
"""返回 (base_origin, default_next_path, embed_kind, error_detail)。"""
|
||||
sid = body.get("service_id")
|
||||
base = (body.get("base_url") or "").strip().rstrip("/")
|
||||
next_path = (body.get("next") or "/dashboard").strip() or "/dashboard"
|
||||
embed_kind = (body.get("embed_kind") or "").strip().lower()
|
||||
if not next_path.startswith("/"):
|
||||
next_path = "/" + next_path
|
||||
|
||||
if sid:
|
||||
svc = db.session.get(Service, int(sid))
|
||||
if not svc or not svc.is_gate_scout_embed():
|
||||
return None, None, None, "服务不存在或未标记为 Gate 扫单嵌入"
|
||||
base = svc.build_origin()
|
||||
embed_kind = (svc.embed_kind or "").strip().lower()
|
||||
if not body.get("next") or next_path == "/dashboard":
|
||||
p = (svc.path or "/dashboard").strip() or "/dashboard"
|
||||
next_path = p if p.startswith("/") else "/" + p
|
||||
|
||||
if not base:
|
||||
return None, None, None, "缺少 base_url"
|
||||
if embed_kind in ("gate_exec", "exec"):
|
||||
login_path = "/login"
|
||||
else:
|
||||
login_path = "/api/auth/login"
|
||||
return base, next_path, login_path, None
|
||||
|
||||
|
||||
def _gate_scout_api_login(
|
||||
base: str, username: str, password: str, *, login_path: str, next_path: str
|
||||
) -> tuple[str | None, str | None]:
|
||||
"""服务端登录 Gate 扫单/执行器,返回 (完整 embed_auth_url, error_detail)。"""
|
||||
payload = json.dumps(
|
||||
{"username": username, "password": password, "embed": "1", "next": next_path}
|
||||
).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{base.rstrip('/')}{login_path}",
|
||||
data=payload,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"X-Nav-Embed": "1",
|
||||
},
|
||||
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 None, detail or "Gate 扫单登录失败"
|
||||
except Exception as e:
|
||||
return None, f"无法连接 Gate 服务: {e}"
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return None, "Gate 服务返回非 JSON"
|
||||
if not data.get("ok"):
|
||||
return None, data.get("detail") or "登录失败"
|
||||
embed_url = (data.get("embed_auth_url") or "").strip()
|
||||
if embed_url.startswith("/"):
|
||||
embed_url = base.rstrip("/") + embed_url
|
||||
if not embed_url:
|
||||
token = (data.get("session_token") or "").strip()
|
||||
if token:
|
||||
q = urlencode({"token": token, "next": next_path, "embed": "1"})
|
||||
embed_url = f"{base.rstrip('/')}/embed-auth?{q}"
|
||||
if not embed_url:
|
||||
return None, "未返回 embed_auth_url,请确认云端已设置 NAV_EMBED_SESSION=1 并重启 PM2"
|
||||
return embed_url, None
|
||||
|
||||
|
||||
def create_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.config["SECRET_KEY"] = os.environ.get("NAV_SECRET_KEY") or secrets.token_hex(32)
|
||||
@@ -265,6 +344,8 @@ def create_app() -> Flask:
|
||||
"index.html",
|
||||
grouped=grouped,
|
||||
hub_auto_login=os.environ.get("NAV_HUB_AUTO_LOGIN", "").strip() == "1",
|
||||
gate_scout_auto_login=os.environ.get("NAV_GATE_SCOUT_AUTO_LOGIN", "").strip()
|
||||
== "1",
|
||||
)
|
||||
|
||||
@app.route("/api/embed/hub-login", methods=["POST"])
|
||||
@@ -297,6 +378,47 @@ def create_app() -> Flask:
|
||||
embed_url = f"{base}/embed-auth?{q}"
|
||||
return jsonify({"ok": True, "embed_auth_url": embed_url, "next": next_path})
|
||||
|
||||
@app.route("/api/embed/gate-scout-login", methods=["POST"])
|
||||
@csrf.exempt
|
||||
@login_required
|
||||
def api_embed_gate_scout_login():
|
||||
"""
|
||||
本地导航代登录 Gate 扫描端/执行器:服务端请求 /api/auth/login 或 /login,
|
||||
再让 iframe 打开 /embed-auth 写入 SameSite=None Cookie。
|
||||
"""
|
||||
body = request.get_json(silent=True) or {}
|
||||
username = (
|
||||
body.get("username") or os.environ.get("NAV_GATE_SCOUT_USERNAME") or ""
|
||||
).strip()
|
||||
password = body.get("password")
|
||||
if password is None:
|
||||
password = os.environ.get("NAV_GATE_SCOUT_PASSWORD") or ""
|
||||
password = str(password)
|
||||
|
||||
base, next_path, login_path, err = _resolve_gate_scout_embed_service(body)
|
||||
if err:
|
||||
return jsonify({"ok": False, "detail": err}), 400
|
||||
if not username or not password:
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"detail": "缺少 Gate 扫单用户名或密码(可配置 NAV_GATE_SCOUT_USERNAME / NAV_GATE_SCOUT_PASSWORD)",
|
||||
}
|
||||
), 400
|
||||
|
||||
embed_url, err = _gate_scout_api_login(
|
||||
base,
|
||||
username,
|
||||
password,
|
||||
login_path=login_path,
|
||||
next_path=next_path,
|
||||
)
|
||||
if err:
|
||||
status = 401 if "登录" in err or "401" in err or "密码" in err else 502
|
||||
return jsonify({"ok": False, "detail": err}), status
|
||||
|
||||
return jsonify({"ok": True, "embed_auth_url": embed_url, "next": next_path})
|
||||
|
||||
@app.route("/api/embed/hub-instance-url", methods=["POST"])
|
||||
@csrf.exempt
|
||||
@login_required
|
||||
@@ -620,12 +742,12 @@ def _ensure_gate_scout_services() -> None:
|
||||
db.session.flush()
|
||||
|
||||
defs = (
|
||||
("Gate 扫描端", scout_host, scout_port, scout_path, 0),
|
||||
("Gate 下单执行器", exec_host, exec_port, exec_path, 10),
|
||||
("Gate 扫描端", scout_host, scout_port, scout_path, 0, "gate_scout"),
|
||||
("Gate 下单执行器", exec_host, exec_port, exec_path, 10, "gate_exec"),
|
||||
)
|
||||
added = 0
|
||||
updated = 0
|
||||
for name, h, port, path, order in defs:
|
||||
for name, h, port, path, order, embed_k in defs:
|
||||
existing = Service.query.filter_by(group_id=g.id, name=name).first()
|
||||
if existing:
|
||||
if update_existing and (
|
||||
@@ -633,11 +755,16 @@ def _ensure_gate_scout_services() -> None:
|
||||
or existing.host != h
|
||||
or existing.port != port
|
||||
or existing.path != path
|
||||
or (existing.embed_kind or "") != embed_k
|
||||
):
|
||||
existing.scheme = scheme
|
||||
existing.host = h
|
||||
existing.port = port
|
||||
existing.path = path
|
||||
existing.embed_kind = embed_k
|
||||
updated += 1
|
||||
elif not (existing.embed_kind or "").strip():
|
||||
existing.embed_kind = embed_k
|
||||
updated += 1
|
||||
continue
|
||||
db.session.add(
|
||||
@@ -649,7 +776,7 @@ def _ensure_gate_scout_services() -> None:
|
||||
path=path,
|
||||
sort_order=order,
|
||||
group_id=g.id,
|
||||
embed_kind="",
|
||||
embed_kind=embed_k,
|
||||
)
|
||||
)
|
||||
added += 1
|
||||
|
||||
@@ -50,6 +50,8 @@ class ServiceForm(FlaskForm):
|
||||
choices=[
|
||||
("", "普通(直接打开路径)"),
|
||||
("hub", "复盘中控(云端 hub,需 embed-auth 登录)"),
|
||||
("gate_scout", "Gate 扫描端(iframe 代登录)"),
|
||||
("gate_exec", "Gate 执行器(iframe 代登录)"),
|
||||
],
|
||||
default="",
|
||||
validators=[Optional()],
|
||||
|
||||
@@ -66,12 +66,12 @@ class Service(db.Model):
|
||||
return f"{self.build_origin()}{p}"
|
||||
|
||||
def build_open_url(self) -> str:
|
||||
"""导航 iframe 首次打开的地址(中控走 login?embed=1 以便写入会话)。"""
|
||||
"""导航 iframe 首次打开的地址(中控 / Gate 扫单走 login?embed=1 以便写入会话)。"""
|
||||
kind = (self.embed_kind or "").strip().lower()
|
||||
next_path = (self.path or "/monitor").strip() or "/monitor"
|
||||
if not next_path.startswith("/"):
|
||||
next_path = "/" + next_path
|
||||
if kind == "hub":
|
||||
if kind == "hub" or kind in ("gate_scout", "gate_exec", "scout", "exec"):
|
||||
from urllib.parse import urlencode
|
||||
|
||||
q = urlencode({"embed": "1", "next": next_path})
|
||||
@@ -80,3 +80,18 @@ class Service(db.Model):
|
||||
|
||||
def is_hub_embed(self) -> bool:
|
||||
return (self.embed_kind or "").strip().lower() == "hub"
|
||||
|
||||
def is_gate_scout_embed(self) -> bool:
|
||||
return (self.embed_kind or "").strip().lower() in (
|
||||
"gate_scout",
|
||||
"gate_exec",
|
||||
"scout",
|
||||
"exec",
|
||||
)
|
||||
|
||||
def gate_scout_api_login_path(self) -> str:
|
||||
"""服务端代登录使用的 API 路径。"""
|
||||
kind = (self.embed_kind or "").strip().lower()
|
||||
if kind in ("gate_exec", "exec"):
|
||||
return "/login"
|
||||
return "/api/auth/login"
|
||||
|
||||
+86
-2
@@ -118,6 +118,15 @@
|
||||
<span class="frame-title" id="current-service-name"></span>
|
||||
</div>
|
||||
<div class="frame-toolbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-toolbar-refresh"
|
||||
id="frame-gate-login"
|
||||
title="通过本地导航代登录 Gate 扫单(需配置 NAV_GATE_SCOUT_USERNAME / NAV_GATE_SCOUT_PASSWORD)"
|
||||
hidden
|
||||
>
|
||||
Gate 登录
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-toolbar-refresh"
|
||||
@@ -159,6 +168,7 @@
|
||||
<script>
|
||||
(function () {
|
||||
var hubAutoLogin = {{ 'true' if hub_auto_login else 'false' }};
|
||||
var gateScoutAutoLogin = {{ 'true' if gate_scout_auto_login else 'false' }};
|
||||
var layoutMain = document.getElementById("layout-main");
|
||||
var btnSidebarCollapse = document.getElementById("sidebar-collapse");
|
||||
var btnSidebarExpand = document.getElementById("sidebar-expand");
|
||||
@@ -201,6 +211,7 @@
|
||||
var btnBack = document.getElementById("frame-back-overview");
|
||||
var btnBackHub = document.getElementById("frame-back-hub");
|
||||
var btnHubLogin = document.getElementById("frame-hub-login");
|
||||
var btnGateLogin = document.getElementById("frame-gate-login");
|
||||
var btnInstanceSso = document.getElementById("frame-instance-sso");
|
||||
var currentBaseUrl = "";
|
||||
var currentOpenUrl = "";
|
||||
@@ -240,6 +251,11 @@
|
||||
return (kind || "").toLowerCase() === "hub";
|
||||
}
|
||||
|
||||
function isGateScoutEmbed(kind) {
|
||||
var k = (kind || "").toLowerCase();
|
||||
return k === "gate_scout" || k === "gate_exec" || k === "scout" || k === "exec";
|
||||
}
|
||||
|
||||
function toggleInstanceBackBtn(show) {
|
||||
if (btnBackHub) btnBackHub.hidden = !show;
|
||||
}
|
||||
@@ -252,6 +268,10 @@
|
||||
if (btnHubLogin) btnHubLogin.hidden = !show;
|
||||
}
|
||||
|
||||
function toggleGateLoginBtn(show) {
|
||||
if (btnGateLogin) btnGateLogin.hidden = !show;
|
||||
}
|
||||
|
||||
function applyIframeUrl(url) {
|
||||
if (!url) return;
|
||||
frame.src = url;
|
||||
@@ -329,6 +349,36 @@
|
||||
});
|
||||
}
|
||||
|
||||
function gateScoutLoginViaProxy(done) {
|
||||
if (!currentServiceId && !currentOrigin) {
|
||||
if (done) done(false, "未选择 Gate 服务");
|
||||
return;
|
||||
}
|
||||
var body = { service_id: parseInt(currentServiceId, 10) || undefined, next: currentNextPath };
|
||||
fetch("/api/embed/gate-scout-login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then(function (r) {
|
||||
return r.json().then(function (j) {
|
||||
return { ok: r.ok, j: j };
|
||||
});
|
||||
})
|
||||
.then(function (res) {
|
||||
if (res.ok && res.j.ok && res.j.embed_auth_url) {
|
||||
applyIframeUrl(res.j.embed_auth_url);
|
||||
toggleGateLoginBtn(false);
|
||||
if (done) done(true);
|
||||
return;
|
||||
}
|
||||
if (done) done(false, (res.j && res.j.detail) || "Gate 登录失败");
|
||||
})
|
||||
.catch(function (e) {
|
||||
if (done) done(false, String(e));
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("message", function (ev) {
|
||||
var data = ev.data;
|
||||
if (!data || !data.type) return;
|
||||
@@ -480,19 +530,36 @@
|
||||
instanceNavCtx = null;
|
||||
hubReturnState = null;
|
||||
toggleInstanceBackBtn(false);
|
||||
toggleGateLoginBtn(false);
|
||||
nameEl.textContent = name || "";
|
||||
dashboard.hidden = true;
|
||||
frameStack.hidden = false;
|
||||
frame.hidden = false;
|
||||
toggleHubLoginBtn(isHubEmbed(currentEmbedKind));
|
||||
toggleHubLoginBtn(isHubEmbed(currentEmbedKind) && !hubAutoLogin);
|
||||
toggleGateLoginBtn(isGateScoutEmbed(currentEmbedKind) && !gateScoutAutoLogin);
|
||||
if (isHubEmbed(currentEmbedKind) && hubAutoLogin) {
|
||||
hubLoginViaProxy(function (ok, err) {
|
||||
if (!ok) applyIframeUrl(url);
|
||||
if (!ok) {
|
||||
applyIframeUrl(url);
|
||||
toggleHubLoginBtn(true);
|
||||
}
|
||||
});
|
||||
var nav = preferredNav || findNavLink(url);
|
||||
setActive(nav);
|
||||
return;
|
||||
}
|
||||
if (isGateScoutEmbed(currentEmbedKind) && gateScoutAutoLogin) {
|
||||
gateScoutLoginViaProxy(function (ok, err) {
|
||||
if (!ok) {
|
||||
applyIframeUrl(url);
|
||||
toggleGateLoginBtn(true);
|
||||
if (err) console.warn("[LocalNav] Gate 代登录失败:", err);
|
||||
}
|
||||
});
|
||||
var navGate = preferredNav || findNavLink(url);
|
||||
setActive(navGate);
|
||||
return;
|
||||
}
|
||||
applyIframeUrl(url);
|
||||
var nav = preferredNav || findNavLink(url);
|
||||
setActive(nav);
|
||||
@@ -573,6 +640,7 @@
|
||||
frameStack.hidden = true;
|
||||
dashboard.hidden = false;
|
||||
toggleHubLoginBtn(false);
|
||||
toggleGateLoginBtn(false);
|
||||
toggleInstanceSsoBtn(false);
|
||||
setActive(null);
|
||||
}
|
||||
@@ -604,6 +672,22 @@
|
||||
});
|
||||
});
|
||||
|
||||
if (btnGateLogin) {
|
||||
btnGateLogin.addEventListener("click", function () {
|
||||
btnGateLogin.disabled = true;
|
||||
gateScoutLoginViaProxy(function (ok, err) {
|
||||
btnGateLogin.disabled = false;
|
||||
if (!ok && err) {
|
||||
window.alert(
|
||||
"Gate 登录失败:\n" +
|
||||
err +
|
||||
"\n\n请检查 LocalNav .env 的 NAV_GATE_SCOUT_USERNAME / NAV_GATE_SCOUT_PASSWORD,以及云端 PM2 的 NAV_EMBED_SESSION / NAV_EMBED_ORIGINS。"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (btnHubLogin) {
|
||||
btnHubLogin.addEventListener("click", function () {
|
||||
btnHubLogin.disabled = true;
|
||||
|
||||
Reference in New Issue
Block a user