Files
iAOP/web/auth/user_store.js
T
bot_dev1 7a2e45e7c4 feat: 全站 Ant Design Pro 风格重构 + 登录守卫与角色门户(issue #131/#134)
- 新增 web/shared:pro.css(AntD 5 设计系统,旧 CSS 变量统一映射 AntD 色板)、
  session.js(后端优先+本地账号降级双轨会话,IAOP_AUTH 兼容层)、
  pro-header.js(全站统一 Pro 导航条:logo/页题/用户角色 Tag/退出)
- 门户 web/index.html:未登录跳登录页(next 回跳),登录后按角色过滤
  可见模块(readonly→驾驶舱/助手/移动端;engineer→+配置台;admin→全部)
- 登录页 auth/login.html 重写为 Ant Design Pro 风格,本地降级模式自动
  播种演示账号(admin/engineer/viewer),保留 OIDC SSO 双轨入口
- user_store.js 新增 verify() + seedDefaults() 默认账号播种
- 六模块页统一接入 pro.css + 统一导航条 + 登录守卫(PRD 8.2)
- 驾驶舱主题 dark→light(Ant Design 5 色板):renderer.py THEME_TOKENS、
  ti-cl4/resin 模板 theme、告警三级配色对齐 AntD(P0 #ff4d4f/P1 #faad14/
  P2 #1677ff),渲染计划已重编(build_plans.py)
- 验证:core/cockpit 86 单测通过;JS 语法检查通过;静态冒烟全 200
2026-08-05 14:17:37 +08:00

156 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}
};
})();