229 lines
8.3 KiB
JavaScript
229 lines
8.3 KiB
JavaScript
// 认证:登录(邮箱密码 / UUID+PLATFORM_KEY 两 tab)、管理员加用户、改密码。
|
||
// 各入口在本模块顶层自绑 onclick;只 logout / closeChpwModal 对外
|
||
// (logout 供全局 401 处理,closeChpwModal 供 main 的 Esc 统一关弹窗栈)。
|
||
// 反向依赖 main 的 glue:enterApp(登录成功进入)、embedPostToParent/embedShowWaiting
|
||
// (logout 在 embed 模式通知父页面)——均运行时(点击/401)才调,ES 环 live binding 安全。
|
||
import { state, LS_TOKEN, LS_UID, LS_NAME, EMBED } from "./state.js";
|
||
import { $ } from "./dom.js";
|
||
import { api } from "./api.js";
|
||
import { enterApp } from "./main.js";
|
||
import { embedPostToParent, embedShowWaiting } from "./embed.js";
|
||
|
||
// ───── login ─────
|
||
let loginTab = "pw"; // "pw" | "key";持久化 last-used tab 在 LS,刷新后默认那个
|
||
const LS_TAB = "zcbot_login_tab";
|
||
function switchLoginTab(name) {
|
||
loginTab = name;
|
||
document.querySelectorAll("#login .tabs button").forEach(b => {
|
||
b.classList.toggle("active", b.dataset.tab === name);
|
||
});
|
||
document.querySelectorAll("#login .tab-body").forEach(b => {
|
||
b.classList.toggle("active", b.id === "body-" + name);
|
||
});
|
||
localStorage.setItem(LS_TAB, name);
|
||
$("li-err").textContent = "";
|
||
// 自动 focus 第一个空 input,Enter 直接登
|
||
const firstInput = document.querySelector("#body-" + name + " input");
|
||
if (firstInput) firstInput.focus();
|
||
}
|
||
document.querySelectorAll("#login .tabs button").forEach(b => {
|
||
b.addEventListener("click", () => switchLoginTab(b.dataset.tab));
|
||
});
|
||
const savedTab = localStorage.getItem(LS_TAB);
|
||
if (savedTab === "key") switchLoginTab("key");
|
||
|
||
$("li-go").onclick = doLogin;
|
||
// 任意 input 上回车都触发登录
|
||
document.querySelectorAll("#login input").forEach(i => {
|
||
i.addEventListener("keydown", (e) => { if (e.key === "Enter") doLogin(); });
|
||
});
|
||
|
||
async function doLogin() {
|
||
$("li-err").textContent = "";
|
||
let url, body, displayLabel;
|
||
if (loginTab === "pw") {
|
||
const email = $("li-email").value.trim();
|
||
const password = $("li-password").value;
|
||
if (!email || !password) {
|
||
$("li-err").textContent = "请填邮箱和密码";
|
||
return;
|
||
}
|
||
url = "/v1/auth/login_password";
|
||
body = { email, password };
|
||
displayLabel = "email";
|
||
} else {
|
||
const uid = $("li-uid").value.trim();
|
||
const pkey = $("li-pkey").value;
|
||
if (!uid || !pkey) {
|
||
$("li-err").textContent = "请填 user_id 和 PLATFORM_KEY";
|
||
return;
|
||
}
|
||
url = "/v1/auth/login";
|
||
body = { user_id: uid, platform_key: pkey };
|
||
displayLabel = null; // 这条路径不返显示名,顶栏只显 uid 前 8 位
|
||
}
|
||
try {
|
||
const r = await fetch(url, {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
if (!r.ok) {
|
||
const d = await r.json().catch(() => ({}));
|
||
// 登录失败:表单已校验非空,非 2xx 绝大多数是凭据不对。
|
||
// 凭据类状态(400/401/403/404 —— 404 多半是前置网关把 403 改写了)统一给友好提示;
|
||
// 5xx 才暴露状态码,提示是服务端问题而非用户输错。
|
||
if (r.status >= 500) {
|
||
throw new Error(`服务器错误,请稍后重试(${r.status})`);
|
||
}
|
||
throw new Error(loginTab === "pw"
|
||
? "账号或密码错误"
|
||
: "user_id 或 PLATFORM_KEY 错误");
|
||
}
|
||
const data = await r.json();
|
||
state.token = data.token;
|
||
state.userId = data.user_id;
|
||
state.userName = displayLabel ? (data[displayLabel] || "") : "";
|
||
localStorage.setItem(LS_TOKEN, state.token);
|
||
localStorage.setItem(LS_UID, state.userId);
|
||
if (state.userName) {
|
||
localStorage.setItem(LS_NAME, state.userName);
|
||
} else {
|
||
localStorage.removeItem(LS_NAME);
|
||
}
|
||
enterApp();
|
||
} catch (e) {
|
||
$("li-err").textContent = e.message;
|
||
}
|
||
}
|
||
|
||
export function logout() {
|
||
state.token = ""; state.userId = ""; state.userName = "";
|
||
localStorage.removeItem(LS_TOKEN);
|
||
localStorage.removeItem(LS_UID);
|
||
localStorage.removeItem(LS_NAME);
|
||
if (state.evtSrc) state.evtSrc.close();
|
||
if (EMBED) {
|
||
embedPostToParent({ type: "zcbot-401" });
|
||
embedShowWaiting("登录已失效,等待父页面重新签发…", false);
|
||
document.body.classList.add("embed-waiting");
|
||
return;
|
||
}
|
||
location.reload();
|
||
}
|
||
$("hd-logout").onclick = logout;
|
||
|
||
// ───── admin add-user ─────
|
||
// 入口在登录页右下角链接;弹窗收 email/password/admin_token 三项,POST /v1/auth/admin/create_user。
|
||
// 成功后不自动登录(让用户自己用新账号登,逻辑清晰),只回填邮箱到登录表单 + 提示。
|
||
function openAdminModal() {
|
||
$("ad-email").value = "";
|
||
$("ad-password").value = "";
|
||
$("ad-token").value = "";
|
||
$("ad-role").value = "user";
|
||
$("ad-err").textContent = "";
|
||
$("admin-modal").classList.add("show");
|
||
$("ad-email").focus();
|
||
}
|
||
function closeAdminModal() {
|
||
$("admin-modal").classList.remove("show");
|
||
}
|
||
$("open-admin-add").onclick = (e) => { e.preventDefault(); openAdminModal(); };
|
||
$("ad-cancel").onclick = closeAdminModal;
|
||
$("admin-modal").addEventListener("click", (e) => {
|
||
if (e.target.id === "admin-modal") closeAdminModal(); // 点遮罩关闭
|
||
});
|
||
// 任一 input 上回车触发提交
|
||
document.querySelectorAll("#admin-modal input").forEach(i => {
|
||
i.addEventListener("keydown", (e) => { if (e.key === "Enter") doAdminAdd(); });
|
||
});
|
||
|
||
async function doAdminAdd() {
|
||
$("ad-err").textContent = "";
|
||
const email = $("ad-email").value.trim();
|
||
const password = $("ad-password").value;
|
||
const admin_token = $("ad-token").value;
|
||
const role = $("ad-role").value;
|
||
if (!email || !password || !admin_token) {
|
||
$("ad-err").textContent = "请填邮箱、密码、管理员口令";
|
||
return;
|
||
}
|
||
if (password.length < 6) {
|
||
$("ad-err").textContent = "密码至少 6 字符";
|
||
return;
|
||
}
|
||
try {
|
||
const r = await fetch("/v1/auth/admin/create_user", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ email, password, admin_token, role }),
|
||
});
|
||
if (!r.ok) {
|
||
const d = await r.json().catch(() => ({}));
|
||
throw new Error(d.detail || (r.status + " create failed"));
|
||
}
|
||
const data = await r.json();
|
||
closeAdminModal();
|
||
// 切到邮箱密码 tab,回填邮箱,提示一下
|
||
switchLoginTab("pw");
|
||
$("li-email").value = data.email || email;
|
||
$("li-password").value = "";
|
||
$("li-password").focus();
|
||
$("li-err").style.color = "var(--muted)"; // 临时降级为提示色
|
||
$("li-err").textContent = `已创建 ${data.email}(${data.role || role}),请登录`;
|
||
setTimeout(() => { $("li-err").style.color = ""; }, 4000);
|
||
} catch (e) {
|
||
$("ad-err").textContent = e.message;
|
||
}
|
||
}
|
||
$("ad-go").onclick = doAdminAdd;
|
||
|
||
// ───── 改密码(顶栏入口,需已登录)─────
|
||
// 旧/新/确认三项;user_id 不传,后端从 JWT 取。成功后关弹窗,提示一下(不登出)。
|
||
function openChpwModal() {
|
||
$("cp-old").value = "";
|
||
$("cp-new").value = "";
|
||
$("cp-new2").value = "";
|
||
$("cp-err").textContent = "";
|
||
$("chpw-modal").classList.add("show");
|
||
$("cp-old").focus();
|
||
}
|
||
export function closeChpwModal() {
|
||
$("chpw-modal").classList.remove("show");
|
||
}
|
||
$("hd-chpw").onclick = openChpwModal;
|
||
$("cp-cancel").onclick = closeChpwModal;
|
||
$("chpw-modal").addEventListener("click", (e) => {
|
||
if (e.target.id === "chpw-modal") closeChpwModal(); // 点遮罩关闭
|
||
});
|
||
document.querySelectorAll("#chpw-modal input").forEach(i => {
|
||
i.addEventListener("keydown", (e) => { if (e.key === "Enter") doChangePassword(); });
|
||
});
|
||
|
||
async function doChangePassword() {
|
||
$("cp-err").textContent = "";
|
||
const oldPw = $("cp-old").value;
|
||
const newPw = $("cp-new").value;
|
||
const newPw2 = $("cp-new2").value;
|
||
if (!oldPw || !newPw || !newPw2) {
|
||
$("cp-err").textContent = "请填旧密码和新密码";
|
||
return;
|
||
}
|
||
if (newPw.length < 6) {
|
||
$("cp-err").textContent = "新密码至少 6 字符";
|
||
return;
|
||
}
|
||
if (newPw !== newPw2) {
|
||
$("cp-err").textContent = "两次输入的新密码不一致";
|
||
return;
|
||
}
|
||
try {
|
||
await api("POST", "/v1/auth/change_password", { old_password: oldPw, new_password: newPw });
|
||
closeChpwModal();
|
||
// 不登出:同一会话 JWT 仍有效,下次登录用新密码即可
|
||
alert("密码已修改,下次登录请用新密码");
|
||
} catch (e) {
|
||
if (e.status === 401) { closeChpwModal(); logout(); return; }
|
||
$("cp-err").textContent = e.message;
|
||
}
|
||
}
|
||
$("cp-go").onclick = doChangePassword;
|