/* iAOP 全站统一会话层(Ant Design Pro 风格重构,2026-08)。 * * 背景:#150 的 auth.js 只对接 core/auth 后端(/auth/login|logout|me), * 后端未部署时登录态完全不可用,门户只能裸奔展示全部模块。 * 本文件提供「后端优先 + 本地降级」双轨会话: * 1. 后端可达(同源 /auth/*)→ 走后端 HMAC 会话(HttpOnly cookie); * 2. 后端不可达(纯静态部署,如 nginx :8090)→ 走 UserStore 本地账号 * (PBKDF2 降级为 salt+SHA-256 的 Demo 级校验,见 user_store.js), * 会话落 localStorage(iaop.session.v1)。 * 两种模式下 currentUser() 返回结构一致:{username, role, active}。 * * 角色→模块映射(PRD 8.2 三级 RBAC,门户按此过滤可见模块): * readonly(Viewer) : 驾驶舱 / 对话助手 / 移动端 * engineer(Editor) : + 模板配置台 * admin(Publisher) : + 管理控制台 / 用户与角色(全部 6 模块) * * 同时导出 window.IAOP_AUTH 兼容层(与 #150 auth.js 同签名), * studio/admin/users 等既有页面无需修改业务代码即可获得本地降级能力。 */ "use strict"; var IAOP_SESSION = (function () { var SESSION_KEY = "iaop.session.v1"; var BACKEND_TIMEOUT_MS = 1500; // /auth/* 探测超时,超时即降级本地 /* ---- 模块注册表(门户卡片 + 访问控制单一数据源) ---------------------- */ var MODULES = [ { key: "cockpit", title: "配置化驾驶舱", icon: "🚀", url: "cockpit/", desc: "四状态工艺流程视图、实时趋势、KPI 卡片、告警面板、NL 查询(PRD 5.5)", roles: ["readonly", "engineer", "admin"], tag: "只读", tagColor: "default" }, { key: "chat", title: "对话助手", icon: "💬", url: "chat/assistant.html", desc: "NL 工艺查询 / 报警智能解释 / 交接班报告生成(PRD 5.4)", roles: ["readonly", "engineer", "admin"], tag: "AI 助手", tagColor: "blue" }, { key: "studio", title: "模板配置台", icon: "🧩", url: "studio/", desc: "点位字典导入 / 模型超参 / RAG 知识库 / 布局编排 / 版本发布(PRD 5.7)", roles: ["engineer", "admin"], tag: "可配置", tagColor: "green", children: [ { key: "import", title: "点位导入", hash: "#import" }, { key: "hyper", title: "模型超参", hash: "#hyper" }, { key: "layout", title: "驾驶舱编排", hash: "#layout" }, { key: "version", title: "版本发布", hash: "#version" } ] }, { key: "admin", title: "管理控制台", icon: "⚙️", url: "admin/", desc: "模型管理(版本/阶段/回滚)、知识库管理、告警确认、审计查询", roles: ["admin"], tag: "管理员", tagColor: "gold", children: [ { key: "models", title: "模型管理", hash: "#models" }, { key: "kb", title: "知识库管理", hash: "#kb" }, { key: "alerts", title: "告警确认", hash: "#alerts" }, { key: "audit", title: "审计查询", hash: "#audit" } ] }, { key: "users", title: "用户与角色", icon: "👤", url: "auth/users.html", desc: "用户管理、角色分配(readonly / engineer / admin)、审计(PRD 8.2)", roles: ["admin"], tag: "管理员", tagColor: "gold" }, { key: "mobile", title: "移动端驾驶舱", icon: "📱", url: "mobile/", desc: "移动端只读驾驶舱与交接班摘要(iOS14+ / Android 10+)", roles: ["readonly", "engineer", "admin"], tag: "移动端", tagColor: "default" } ]; var ROLE_LABELS = { readonly: "Viewer · 只读", engineer: "Editor · 配置", admin: "Publisher · 管理" }; var ROLE_TAG_COLORS = { readonly: "default", engineer: "green", admin: "gold" }; /* ---- 本地会话存取 ------------------------------------------------------ */ function saveSession(user) { try { localStorage.setItem(SESSION_KEY, JSON.stringify({ username: user.username, role: user.role, loginAt: new Date().toISOString() })); } catch (e) { /* 隐私模式等场景忽略 */ } } function readSession() { try { var v = localStorage.getItem(SESSION_KEY); return v ? JSON.parse(v) : null; } catch (e) { return null; } } function clearSession() { try { localStorage.removeItem(SESSION_KEY); } catch (e) {} } /* ---- 后端探测(带超时) ------------------------------------------------ */ function fetchWithTimeout(url, opts) { return new Promise(function (resolve, reject) { var timer = setTimeout(function () { reject(new Error("timeout")); }, BACKEND_TIMEOUT_MS); fetch(url, opts).then(function (r) { clearTimeout(timer); resolve(r); }, function (e) { clearTimeout(timer); reject(e); }); }); } /* ---- 当前登录用户:后端优先,本地会话降级 ------------------------------ */ function currentUser() { return fetchWithTimeout("/auth/me", { credentials: "include" }) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (d) { if (d && d.user) return d.user; throw new Error("no backend session"); }) .catch(function () { // 后端不可达或无后端会话 → 本地会话降级,并用 UserStore 复核用户仍有效 var s = readSession(); if (!s || !s.username) return null; if (typeof UserStore === "undefined") return s; // 无存储层时信任会话 var u = UserStore.find(s.username); if (!u || !u.active) { clearSession(); return null; } return { username: u.username, role: u.role, active: u.active }; }); } /* ---- 登录:后端优先,本地账号降级 -------------------------------------- */ function login(username, password) { return fetchWithTimeout("/auth/login", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username, password: password }) }).then(function (r) { if (r.ok) return r.json().then(function (d) { saveSession(d.user); return d.user; }); // 4xx = 后端在线且拒绝(真实鉴权失败);5xx/网关错误 = 后端不可用,降级本地 if (r.status >= 400 && r.status < 500) { return r.text().then(function (t) { var msg = "用户名或密码错误"; try { msg = (JSON.parse(t) || {}).error || msg; } catch (e) {} var err = new Error(msg); err.authReject = true; throw err; }); } throw new Error("backend-http-" + r.status); }).catch(function (e) { if (e && e.authReject) throw e; // 后端鉴权失败,不降级 // 超时 / 网络错误 / 5xx / 响应非 JSON 等一切后端不可用情形 → 本地账号校验 if (typeof UserStore === "undefined") { throw new Error("认证服务不可用,且本地账号存储未加载"); } return UserStore.seedDefaults().then(function () { return UserStore.verify(username, password); }).then(function (u) { if (!u) throw new Error("用户名或密码错误"); saveSession(u); return u; }); }); } /* ---- 登出:双轨都清 ----------------------------------------------------- */ function logout(loginUrl) { clearSession(); try { fetch("/auth/logout", { method: "POST", credentials: "include" }) .catch(function () {}); } catch (e) {} var next = encodeURIComponent(location.pathname + location.search); location.href = (loginUrl || "../auth/login.html") + "?next=" + next; } /* ---- 路由守卫(与 #150 IAOP_AUTH 同语义) ------------------------------- */ function requireLoginElseRedirect(loginUrl) { return currentUser().then(function (u) { if (!u) { var next = encodeURIComponent(location.pathname + location.search); location.href = (loginUrl || "../auth/login.html") + "?next=" + next; return false; } return u; }); } /* ---- 角色→模块 ---------------------------------------------------------- */ function modulesFor(role) { return MODULES.filter(function (m) { return m.roles.indexOf(role) >= 0; }); } function canAccess(moduleKey, role) { var m = MODULES.find(function (x) { return x.key === moduleKey; }); return !!m && m.roles.indexOf(role) >= 0; } return { MODULES: MODULES, ROLE_LABELS: ROLE_LABELS, ROLE_TAG_COLORS: ROLE_TAG_COLORS, currentUser: currentUser, login: login, logout: logout, requireLoginElseRedirect: requireLoginElseRedirect, modulesFor: modulesFor, canAccess: canAccess }; })(); /* 兼容层:与 #150 auth.js 导出同签名(studio/admin/users 直接复用)。 * 若页面同时加载了 auth.js,本兼容层在后加载时覆盖之,获得本地降级能力。 */ window.IAOP_AUTH = { currentUser: IAOP_SESSION.currentUser, requireLoginElseRedirect: IAOP_SESSION.requireLoginElseRedirect, AUTH_BASE: "" };