/* iAOP 用户/角色存储 + 审计 + OIDC 配置点(issue #134 / PRD 8.2)。 * * 与 #150 的 auth.js 分工: * - auth.js(#150)负责「登录会话」——对接 core/auth 后端 /auth/login|logout|me; * - 本文件(#134)负责「用户/角色管理 + 写操作审计 + OIDC 配置点」—— * core/auth 目前只有 login/logout/me 端点,用户 CRUD 端点尚未提供, * 故管理数据先落 localStorage(Demo 级,User 模型对齐 core/auth/users.py: * username/role/active + 密码 PBKDF2 由后端接管;此处仅存salt+SHA-256 哈希), * 后端补齐 /auth/users CRUD 后仅需替换本文件三处读写。 */ "use strict"; var UserStore = (function () { var USERS_KEY = "iaop.users.v1"; var AUDIT_KEY = "iaop.audit.v1"; var OIDC_KEY = "iaop.oidc.v1"; var ROLES = ["readonly", "engineer", "admin"]; // 对齐 rbac.py RoleKind var ROLE_LABELS = { readonly: "Viewer(只读)", engineer: "Editor(配置)", admin: "Publisher(发布+回滚)" }; function load(key, fallback) { try { var v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; } catch (e) { return fallback; } } function save(key, obj) { localStorage.setItem(key, JSON.stringify(obj)); } function randomHex(n) { var buf = new Uint8Array(n); crypto.getRandomValues(buf); return Array.from(buf).map(function (b) { return b.toString(16).padStart(2, "0"); }).join(""); } function hashPassword(password, salt) { var data = new TextEncoder().encode(salt + ":" + password); return crypto.subtle.digest("SHA-256", data).then(function (buf) { return Array.from(new Uint8Array(buf)) .map(function (b) { return b.toString(16).padStart(2, "0"); }).join(""); }); } /* 默认演示账号(Ant Design Pro 风格重构新增):纯静态部署(无 core/auth * 后端)时的开箱登录账号,首次进入登录页自动播种。生产环境登录后请立即 * 在用户管理页改密或删除。密码均 ≥ 8 位(对齐 auth.js 前端校验)。 */ var DEFAULT_ACCOUNTS = [ { username: "admin", password: "Admin@12345", role: "admin" }, { username: "engineer", password: "Engineer@12", role: "engineer" }, { username: "viewer", password: "Viewer@1234", role: "readonly" } ]; return { ROLES: ROLES, ROLE_LABELS: ROLE_LABELS, DEFAULT_ACCOUNTS: DEFAULT_ACCOUNTS, /* ---------- 用户 CRUD(对齐 core/auth/users.py 的 User 字段语义) ---------- */ list: function () { return load(USERS_KEY, []); }, find: function (username) { return this.list().find(function (u) { return u.username === username; }) || null; }, create: function (username, password, role) { var self = this; if (!/^[a-zA-Z0-9_.-]{2,32}$/.test(username)) { return Promise.reject(new Error("用户名需 2-32 位字母数字._-")); } if (this.find(username)) return Promise.reject(new Error("用户名已存在")); if (ROLES.indexOf(role) < 0) return Promise.reject(new Error("非法角色")); if ((password || "").length < 6) return Promise.reject(new Error("密码至少 6 位")); var salt = randomHex(16); return hashPassword(password, salt).then(function (hash) { var users = self.list(); users.push({ username: username, role: role, active: true, salt: salt, hash: hash, created_at: new Date().toISOString() }); save(USERS_KEY, users); self.audit("create", "user", username + " 角色=" + role); }); }, update: function (username, patch, actor) { var users = this.list(); var u = users.find(function (x) { return x.username === username; }); if (!u) return Promise.reject(new Error("用户不存在")); var self = this; var done = Promise.resolve(); if (patch.role) { if (ROLES.indexOf(patch.role) < 0) return Promise.reject(new Error("非法角色")); u.role = patch.role; } if (patch.active !== undefined) u.active = !!patch.active; if (patch.password) { if (patch.password.length < 6) return Promise.reject(new Error("密码至少 6 位")); u.salt = randomHex(16); done = hashPassword(patch.password, u.salt).then(function (h) { u.hash = h; }); } return done.then(function () { save(USERS_KEY, users); self.audit("update", "user", username + " " + JSON.stringify( { role: patch.role, active: patch.active, password: !!patch.password }), actor); }); }, remove: function (username, actor) { save(USERS_KEY, this.list().filter(function (u) { return u.username !== username; })); this.audit("delete", "user", username, actor); return Promise.resolve(); }, /* ---------- 登录校验 + 默认账号播种(纯静态部署降级,见 shared/session.js) ---------- */ /* 校验用户名密码,成功返回 {username, role, active},失败返回 null。 * 不区分「用户不存在/密码错误/已禁用」,防用户名枚举。 */ verify: function (username, password) { var u = this.find(username); if (!u || !u.active) return Promise.resolve(null); return hashPassword(password, u.salt).then(function (h) { return h === u.hash ? { username: u.username, role: u.role, active: u.active } : null; }); }, /* 用户库为空时播种默认演示账号(幂等:已有任何用户则跳过)。 */ seedDefaults: function () { var self = this; if (this.list().length > 0) return Promise.resolve(false); return DEFAULT_ACCOUNTS.reduce(function (p, acc) { return p.then(function () { return self.create(acc.username, acc.password, acc.role); }); }, Promise.resolve()).then(function () { return true; }); }, /* ---------- 写操作审计(template_registry._log 风格:时间/操作人/动作/资源/原因) ---------- */ audit: function (action, resource, reason, actor) { var log = load(AUDIT_KEY, []); log.push({ time: new Date().toISOString(), actor: actor || "system", action: action, resource: resource, reason: reason || "" }); if (log.length > 500) log = log.slice(-500); save(AUDIT_KEY, log); }, auditLog: function () { return load(AUDIT_KEY, []); }, /* ---------- OIDC SSO 配置点(PRD 8.2 双轨:未配置走本地账号) ---------- */ configureOidc: function (cfg, actor) { save(OIDC_KEY, cfg); this.audit("update", "oidc_config", cfg.issuer || "", actor); }, oidcConfig: function () { return load(OIDC_KEY, null); }, /* 标准 OIDC 授权码流程 authorize URL;企业 IAM 就绪后直接可用 */ oidcAuthorizeUrl: function (redirectPath) { var cfg = this.oidcConfig(); if (!cfg || !cfg.issuer || !cfg.client_id) return null; var redirect = cfg.redirect_uri || (location.origin + (redirectPath || "/auth/login.html")); var state = randomHex(8); try { sessionStorage.setItem("iaop.oidc.state", state); } catch (e) {} return cfg.issuer.replace(/\/$/, "") + "/authorize?response_type=code" + "&client_id=" + encodeURIComponent(cfg.client_id) + "&redirect_uri=" + encodeURIComponent(redirect) + "&scope=openid%20profile&state=" + state; } }; })();