Files
iAOP/web/auth/user_store.js
T

156 lines
7.3 KiB
JavaScript
Raw Normal View History

/* 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;
}
};
})();