Files
iAOP/web/auth/user_store.js
T

124 lines
5.6 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("");
});
}
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;
}
};
})();