From 73056ed32ec1f23767c249f9066c77c996367258 Mon Sep 17 00:00:00 2001 From: bot_dev1 Date: Wed, 5 Aug 2026 02:08:24 +0000 Subject: [PATCH] =?UTF-8?q?feat(#150):=20=E7=99=BB=E5=BD=95=E9=A1=B5?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=20auth.js=EF=BC=88login/me=20+=20?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E5=AE=88=E5=8D=AB=20requireLoginElseRedirect?= =?UTF-8?q?=EF=BC=8CPRD=208.2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/auth/auth.js | 108 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 web/auth/auth.js diff --git a/web/auth/auth.js b/web/auth/auth.js new file mode 100644 index 0000000..b8dd4ff --- /dev/null +++ b/web/auth/auth.js @@ -0,0 +1,108 @@ +/* iAOP 登录页客户端(issue #150 / PRD 8.2)。 + * 对接 core/auth/auth_api.py: + * POST /auth/login {username,password} → {token,user}(后端 Set-Cookie iaop_session) + * GET /auth/me 凭 cookie 校验当前登录态(路由守卫用) + * 写操作守卫:未登录跳登录页;readonly 角色写按钮置灰(见 studio/cockpit 的 applyRbac)。 + */ +"use strict"; + +// 认证服务基址:独立运行 auth_api 时指向它;由统一网关接入时留空(同源)。 +var AUTH_BASE = (function () { + try { + if (localStorage.getItem("iaop_auth_base")) return localStorage.getItem("iaop_auth_base"); + } catch (e) {} + return ""; // 生产同源,留空 +})(); + +function $(id) { return document.getElementById(id); } + +function showAlert(msg, kind) { + var el = $("alert"); + el.textContent = msg || ""; + el.className = "alert show " + (kind === "ok" ? "alert-ok" : "alert-error"); +} +function clearAlert() { $("alert").className = "alert"; } + +// 检查当前登录态(路由守卫复用)。返回 Promise。 +function currentUser() { + return fetch(join(AUTH_BASE, "/auth/me"), { credentials: "include" }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (d) { return (d && d.user) ? d.user : null; }) + .catch(function () { return null; }); +} + +// 写操作守卫:未登录跳登录页(PRD 8.2)。 +function requireLoginElseRedirect(loginUrl) { + return currentUser().then(function (u) { + if (!u) { + var next = encodeURIComponent(location.pathname + location.search); + location.href = (loginUrl || "login.html") + "?next=" + next; + return false; + } + return u; + }); +} + +function join(base, path) { return (base || "") + path; } + +function login(username, password) { + $("loginBtn").disabled = true; + clearAlert(); + return fetch(join(AUTH_BASE, "/auth/login"), { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: username, password: password }) + }).then(function (resp) { + return resp.json().then(function (d) { + if (!resp.ok) throw new Error((d && d.error) || ("登录失败 HTTP " + resp.status)); + return d; + }); + }).then(function (d) { + // 后端已 Set-Cookie(HttpOnly),同时返回 token 供 Bearer 场景(如 SPA fetch 显式带) + showAlert("登录成功,欢迎 " + (d.user && d.user.username) + "(" + (d.user && d.user.role) + ")", "ok"); + var next = new URLSearchParams(location.search).get("next") || "../cockpit/index.html"; + setTimeout(function () { location.href = next; }, 500); + }).catch(function (e) { + $("password").setAttribute("aria-invalid", "true"); + showAlert(e.message || "登录失败", "error"); + }).finally(function () { + $("loginBtn").disabled = false; + }); +} + +document.addEventListener("DOMContentLoaded", function () { + var form = $("loginForm"); + if (!form) return; + + // 已登录直接跳转 + currentUser().then(function (u) { + if (u) { + var next = new URLSearchParams(location.search).get("next") || "../cockpit/index.html"; + location.href = next; + } + }); + + form.addEventListener("submit", function (e) { + e.preventDefault(); + var u = $("username").value.trim(); + var p = $("password").value; + $("username").removeAttribute("aria-invalid"); + $("password").removeAttribute("aria-invalid"); + if (!u || !p || p.length < 8) { + $("password").setAttribute("aria-invalid", String(!p || p.length < 8)); + showAlert("用户名不能为空且密码不少于 8 位", "error"); + return; + } + login(u, p); + }); + + var tog = $("togglePwd"); + if (tog) tog.addEventListener("click", function () { + var pwd = $("password"); + pwd.type = pwd.type === "password" ? "text" : "password"; + }); +}); + +// 导出给其它页面复用(路由守卫) +window.IAOP_AUTH = { currentUser: currentUser, requireLoginElseRedirect: requireLoginElseRedirect, AUTH_BASE: AUTH_BASE };