zcbot/web/static/js/auth.js

227 lines
8.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.

// 认证:登录(邮箱密码 / 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, LS_USERNAME, LS_EMAIL, EMBED, setIdentity } 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;
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 };
} 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 };
}
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;
localStorage.setItem(LS_TOKEN, state.token);
// 身份字段写 state + LS:platform_key 路径返 name/user_name,邮箱密码路径返 email;
// 缺的走 setIdentity 的兜底(顶栏 userDisplayName)。/v1/me(loadRole)随后再校准一次。
setIdentity({
user_id: data.user_id,
name: data.name,
user_name: data.user_name,
email: data.email,
});
enterApp();
} catch (e) {
$("li-err").textContent = e.message;
}
}
export function logout() {
state.token = "";
localStorage.removeItem(LS_TOKEN);
// 清身份(state + LS_UID/NAME/USERNAME/EMAIL),不残留上一个用户
setIdentity({ user_id: "" });
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;