/* 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(""); }); } return { ROLES: ROLES, ROLE_LABELS: ROLE_LABELS, /* ---------- 用户 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(); }, /* ---------- 写操作审计(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; } }; })();