zcbot/web/static/js/auth.js

221 lines
7.9 KiB
JavaScript
Raw Permalink 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, 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(() => ({}));
throw new Error(d.detail || (r.status + " login failed"));
}
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;