mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:30:48 +08:00
Show login progress immediately and stop auth 401 hangs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f92cb85a4a
commit
5bcaf7fd2d
10 changed files with 152 additions and 74 deletions
|
|
@ -779,7 +779,7 @@
|
||||||
<script defer src="/admin/assets/vendor/marked.min.js"></script>
|
<script defer src="/admin/assets/vendor/marked.min.js"></script>
|
||||||
<script defer src="/admin/assets/vendor/purify.min.js"></script>
|
<script defer src="/admin/assets/vendor/purify.min.js"></script>
|
||||||
<script defer src="/admin/assets/admin-theme.js"></script>
|
<script defer src="/admin/assets/admin-theme.js"></script>
|
||||||
<script type="module" src="/admin/assets/chat.js?v=20260812-chat-2" onerror="(function(){var lang='zh';try{lang=String(localStorage.getItem('ops_admin_lang')||'zh').toLowerCase();}catch(e){}if(lang!=='en')lang='zh';var msg=lang==='en'?{t:'Failed to load chat.js',h:'Confirm the gateway is up and /admin/assets/chat.js is reachable.'}:{t:'无法加载 chat.js',h:'请确认网关已启动且 /admin/assets/chat.js 可访问。'};var s=document.getElementById('chat-boot-splash');if(s){var a=s.querySelector('.chat-boot-splash__title span:last-child');var b=s.querySelector('.chat-boot-splash__muted');if(a)a.textContent=msg.t;if(b)b.textContent=msg.h;}})()"></script>
|
<script type="module" src="/admin/assets/chat.js?v=20260812-chat-3" onerror="(function(){var lang='zh';try{lang=String(localStorage.getItem('ops_admin_lang')||'zh').toLowerCase();}catch(e){}if(lang!=='en')lang='zh';var msg=lang==='en'?{t:'Failed to load chat.js',h:'Confirm the gateway is up and /admin/assets/chat.js is reachable.'}:{t:'无法加载 chat.js',h:'请确认网关已启动且 /admin/assets/chat.js 可访问。'};var s=document.getElementById('chat-boot-splash');if(s){var a=s.querySelector('.chat-boot-splash__title span:last-child');var b=s.querySelector('.chat-boot-splash__muted');if(a)a.textContent=msg.t;if(b)b.textContent=msg.h;}})()"></script>
|
||||||
<!-- Mermaid is larger; load after chat.js so login is not delayed by parse. -->
|
<!-- Mermaid is larger; load after chat.js so login is not delayed by parse. -->
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>oliver</title>
|
<title>oliver</title>
|
||||||
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
|
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
|
||||||
<link rel="stylesheet" href="/admin/assets/styles.css?v=20260812-18" />
|
<link rel="stylesheet" href="/admin/assets/styles.css?v=20260812-19" />
|
||||||
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css?v=20260812-18" />
|
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css?v=20260812-19" />
|
||||||
<link rel="stylesheet" href="/admin/assets/theme-presets.css?v=20260812-18" />
|
<link rel="stylesheet" href="/admin/assets/theme-presets.css?v=20260812-19" />
|
||||||
<style>
|
<style>
|
||||||
/* First paint before app.js runs: keep admin from showing a blank dark screen. */
|
/* First paint before app.js runs: keep admin from showing a blank dark screen. */
|
||||||
.admin-boot-splash {
|
.admin-boot-splash {
|
||||||
|
|
@ -129,33 +129,33 @@
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"/admin/assets/app.js": "/admin/assets/app.js?v=20260812-18",
|
"/admin/assets/app.js": "/admin/assets/app.js?v=20260812-19",
|
||||||
"/admin/assets/js/asset-v.js": "/admin/assets/js/asset-v.js?v=20260812-18",
|
"/admin/assets/js/asset-v.js": "/admin/assets/js/asset-v.js?v=20260812-19",
|
||||||
"/admin/assets/js/core.js": "/admin/assets/js/core.js?v=20260812-18",
|
"/admin/assets/js/core.js": "/admin/assets/js/core.js?v=20260812-19",
|
||||||
"/admin/assets/js/i18n/index.js": "/admin/assets/js/i18n/index.js?v=20260812-18",
|
"/admin/assets/js/i18n/index.js": "/admin/assets/js/i18n/index.js?v=20260812-19",
|
||||||
"/admin/assets/js/i18n/messages-en.js": "/admin/assets/js/i18n/messages-en.js?v=20260812-18",
|
"/admin/assets/js/i18n/messages-en.js": "/admin/assets/js/i18n/messages-en.js?v=20260812-19",
|
||||||
"/admin/assets/js/i18n/messages-zh.js": "/admin/assets/js/i18n/messages-zh.js?v=20260812-18",
|
"/admin/assets/js/i18n/messages-zh.js": "/admin/assets/js/i18n/messages-zh.js?v=20260812-19",
|
||||||
"/admin/assets/js/main.js": "/admin/assets/js/main.js?v=20260812-18",
|
"/admin/assets/js/main.js": "/admin/assets/js/main.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/attachments.js": "/admin/assets/js/pages/attachments.js?v=20260812-18",
|
"/admin/assets/js/pages/attachments.js": "/admin/assets/js/pages/attachments.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/audit.js": "/admin/assets/js/pages/audit.js?v=20260812-18",
|
"/admin/assets/js/pages/audit.js": "/admin/assets/js/pages/audit.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/authz.js": "/admin/assets/js/pages/authz.js?v=20260812-18",
|
"/admin/assets/js/pages/authz.js": "/admin/assets/js/pages/authz.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/legacy-admin.js": "/admin/assets/js/pages/legacy-admin.js?v=20260812-18",
|
"/admin/assets/js/pages/legacy-admin.js": "/admin/assets/js/pages/legacy-admin.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/legacy.js": "/admin/assets/js/pages/legacy.js?v=20260812-18",
|
"/admin/assets/js/pages/legacy.js": "/admin/assets/js/pages/legacy.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/login.js": "/admin/assets/js/pages/login.js?v=20260812-18",
|
"/admin/assets/js/pages/login.js": "/admin/assets/js/pages/login.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/memory.js": "/admin/assets/js/pages/memory.js?v=20260812-18",
|
"/admin/assets/js/pages/memory.js": "/admin/assets/js/pages/memory.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/models.js": "/admin/assets/js/pages/models.js?v=20260812-18",
|
"/admin/assets/js/pages/models.js": "/admin/assets/js/pages/models.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/plugins.js": "/admin/assets/js/pages/plugins.js?v=20260812-18",
|
"/admin/assets/js/pages/plugins.js": "/admin/assets/js/pages/plugins.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/profile.js": "/admin/assets/js/pages/profile.js?v=20260812-18",
|
"/admin/assets/js/pages/profile.js": "/admin/assets/js/pages/profile.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/scheduled-jobs.js": "/admin/assets/js/pages/scheduled-jobs.js?v=20260812-18",
|
"/admin/assets/js/pages/scheduled-jobs.js": "/admin/assets/js/pages/scheduled-jobs.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/skills.js": "/admin/assets/js/pages/skills.js?v=20260812-18",
|
"/admin/assets/js/pages/skills.js": "/admin/assets/js/pages/skills.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/stack.js": "/admin/assets/js/pages/stack.js?v=20260812-18",
|
"/admin/assets/js/pages/stack.js": "/admin/assets/js/pages/stack.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/users.js": "/admin/assets/js/pages/users.js?v=20260812-18",
|
"/admin/assets/js/pages/users.js": "/admin/assets/js/pages/users.js?v=20260812-19",
|
||||||
"/admin/assets/js/pages/workspace-paths.js": "/admin/assets/js/pages/workspace-paths.js?v=20260812-18",
|
"/admin/assets/js/pages/workspace-paths.js": "/admin/assets/js/pages/workspace-paths.js?v=20260812-19",
|
||||||
"/admin/assets/js/router.js": "/admin/assets/js/router.js?v=20260812-18",
|
"/admin/assets/js/router.js": "/admin/assets/js/router.js?v=20260812-19",
|
||||||
"/admin/assets/js/state.js": "/admin/assets/js/state.js?v=20260812-18"
|
"/admin/assets/js/state.js": "/admin/assets/js/state.js?v=20260812-19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/admin/assets/app.js?v=20260812-18" onerror="(function(){var lang='zh';try{lang=String(localStorage.getItem('ops_admin_lang')||'zh').toLowerCase();}catch(e){}if(lang!=='en')lang='zh';var msg=lang==='en'?{t:'Failed to load admin scripts',h:'Confirm /admin/assets/app.js and /admin/assets/js/ are reachable.'}:{t:'无法加载管理台脚本',h:'请确认 /admin/assets/app.js 与 /admin/assets/js/ 可访问。'};var s=document.getElementById('admin-boot-splash');if(s){var a=s.querySelector('.admin-boot-splash__title span:last-child');var b=s.querySelector('.admin-boot-splash__muted');if(a)a.textContent=msg.t;if(b)b.textContent=msg.h;}})()"></script>
|
<script type="module" src="/admin/assets/app.js?v=20260812-19" onerror="(function(){var lang='zh';try{lang=String(localStorage.getItem('ops_admin_lang')||'zh').toLowerCase();}catch(e){}if(lang!=='en')lang='zh';var msg=lang==='en'?{t:'Failed to load admin scripts',h:'Confirm /admin/assets/app.js and /admin/assets/js/ are reachable.'}:{t:'无法加载管理台脚本',h:'请确认 /admin/assets/app.js 与 /admin/assets/js/ 可访问。'};var s=document.getElementById('admin-boot-splash');if(s){var a=s.querySelector('.admin-boot-splash__title span:last-child');var b=s.querySelector('.admin-boot-splash__muted');if(a)a.textContent=msg.t;if(b)b.textContent=msg.h;}})()"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@
|
||||||
* - interfaces/admin/static/index.html importmap + CSS/JS ?v=
|
* - interfaces/admin/static/index.html importmap + CSS/JS ?v=
|
||||||
* - interfaces/admin/static/app.js entry query (optional if importmap covers it)
|
* - interfaces/admin/static/app.js entry query (optional if importmap covers it)
|
||||||
*/
|
*/
|
||||||
export const ADMIN_ASSET_V = "20260812-18";
|
export const ADMIN_ASSET_V = "20260812-19";
|
||||||
|
|
|
||||||
|
|
@ -1073,7 +1073,18 @@ async function apiPost(path, body) {
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(body ?? {}),
|
body: JSON.stringify(body ?? {}),
|
||||||
});
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = text ? JSON.parse(text) : null;
|
||||||
|
} catch (_) {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
|
const u = String(path || "");
|
||||||
|
if (u.includes("/admin/api/auth/login") || u.includes("/admin/api/auth/bootstrap")) {
|
||||||
|
return data && typeof data === "object" ? data : { ok: false, error: "unauthorized" };
|
||||||
|
}
|
||||||
localStorage.removeItem(AUTH_TOKEN_KEY);
|
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||||
localStorage.removeItem(AUTH_SESSION_KEY);
|
localStorage.removeItem(AUTH_SESSION_KEY);
|
||||||
state.authSession = null;
|
state.authSession = null;
|
||||||
|
|
@ -1081,7 +1092,7 @@ async function apiPost(path, body) {
|
||||||
return await new Promise(() => {});
|
return await new Promise(() => {});
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(`POST ${path} ${res.status}`);
|
if (!res.ok) throw new Error(`POST ${path} ${res.status}`);
|
||||||
return await res.json();
|
return data ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiPatch(path, body) {
|
async function apiPatch(path, body) {
|
||||||
|
|
@ -2890,27 +2901,56 @@ async function renderLogin() {
|
||||||
const username = el("input", { class: "input", placeholder: t("auth.username") });
|
const username = el("input", { class: "input", placeholder: t("auth.username") });
|
||||||
const password = el("input", { class: "input", type: "password", placeholder: t("auth.password") });
|
const password = el("input", { class: "input", type: "password", placeholder: t("auth.password") });
|
||||||
const status = el("div", { class: "muted", text: "" });
|
const status = el("div", { class: "muted", text: "" });
|
||||||
|
const btn = el("button", {
|
||||||
|
class: "btn btn--primary",
|
||||||
|
type: "button",
|
||||||
|
text: t("auth.login"),
|
||||||
|
});
|
||||||
|
let busy = false;
|
||||||
|
const setBusy = (on) => {
|
||||||
|
busy = !!on;
|
||||||
|
btn.disabled = busy;
|
||||||
|
btn.textContent = busy ? t("auth.loggingIn") : t("auth.login");
|
||||||
|
};
|
||||||
const doLogin = async () => {
|
const doLogin = async () => {
|
||||||
const resp = await apiPost("/admin/api/auth/login", {
|
if (busy) return;
|
||||||
tenant_id: "",
|
setBusy(true);
|
||||||
username: username.value.trim().toLowerCase(),
|
status.textContent = t("auth.loggingIn");
|
||||||
password: password.value.trim(),
|
try {
|
||||||
purpose: "chat",
|
const resp = await apiPost("/admin/api/auth/login", {
|
||||||
});
|
tenant_id: "",
|
||||||
if (!resp.ok || !resp.token) {
|
username: username.value.trim().toLowerCase(),
|
||||||
const err = String(resp.error || "");
|
password: password.value.trim(),
|
||||||
status.textContent =
|
purpose: "chat",
|
||||||
err === "user_disabled"
|
});
|
||||||
? t("auth.disabled")
|
if (!resp || !resp.ok || !resp.token) {
|
||||||
: err === "admin_role_required"
|
const err = String((resp && resp.error) || "");
|
||||||
? t("auth.chatLoginDenied")
|
status.textContent =
|
||||||
: t("auth.invalid");
|
err === "user_disabled"
|
||||||
return;
|
? t("auth.disabled")
|
||||||
|
: err === "admin_role_required"
|
||||||
|
? t("auth.chatLoginDenied")
|
||||||
|
: t("auth.invalid");
|
||||||
|
setBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem(AUTH_TOKEN_KEY, String(resp.token));
|
||||||
|
localStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(resp.session || {}));
|
||||||
|
state.authSession = resp.session || null;
|
||||||
|
mount(
|
||||||
|
el("div", { class: "chat-app--login" }, [
|
||||||
|
el("div", { class: "card u-modal-card-sm" }, [
|
||||||
|
el("div", { class: "card__title", text: t("auth.login") }),
|
||||||
|
el("div", { class: "muted", text: t("chat.loading") }),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
|
await boot();
|
||||||
|
} catch (err) {
|
||||||
|
status.textContent = String((err && err.message) || err || t("auth.invalid"));
|
||||||
|
setBusy(false);
|
||||||
}
|
}
|
||||||
localStorage.setItem(AUTH_TOKEN_KEY, String(resp.token));
|
|
||||||
localStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(resp.session || {}));
|
|
||||||
state.authSession = resp.session || null;
|
|
||||||
await boot();
|
|
||||||
};
|
};
|
||||||
const onEnterLogin = (ev) => {
|
const onEnterLogin = (ev) => {
|
||||||
if (ev.key !== "Enter") return;
|
if (ev.key !== "Enter") return;
|
||||||
|
|
@ -2919,10 +2959,9 @@ async function renderLogin() {
|
||||||
};
|
};
|
||||||
username.addEventListener("keydown", onEnterLogin);
|
username.addEventListener("keydown", onEnterLogin);
|
||||||
password.addEventListener("keydown", onEnterLogin);
|
password.addEventListener("keydown", onEnterLogin);
|
||||||
const btn = el("button", {
|
btn.addEventListener("click", (ev) => {
|
||||||
class: "btn btn--primary",
|
ev.preventDefault();
|
||||||
text: t("auth.login"),
|
doLogin();
|
||||||
onclick: doLogin,
|
|
||||||
});
|
});
|
||||||
const card = el("div", { class: "card u-modal-card-sm" }, [
|
const card = el("div", { class: "card u-modal-card-sm" }, [
|
||||||
el("div", { class: "card__title", text: t("auth.login") }),
|
el("div", { class: "card__title", text: t("auth.login") }),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export default {
|
||||||
"auth.disabled": "Account is disabled",
|
"auth.disabled": "Account is disabled",
|
||||||
"auth.invalid": "Login failed",
|
"auth.invalid": "Login failed",
|
||||||
"auth.login": "Login",
|
"auth.login": "Login",
|
||||||
|
"auth.loggingIn": "Signing in…",
|
||||||
"auth.logout": "Logout",
|
"auth.logout": "Logout",
|
||||||
"auth.password": "Password",
|
"auth.password": "Password",
|
||||||
"auth.username": "Username",
|
"auth.username": "Username",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export default {
|
||||||
"auth.disabled": "账号已被禁用,请联系管理员",
|
"auth.disabled": "账号已被禁用,请联系管理员",
|
||||||
"auth.invalid": "登录失败,请检查凭据",
|
"auth.invalid": "登录失败,请检查凭据",
|
||||||
"auth.login": "登录",
|
"auth.login": "登录",
|
||||||
|
"auth.loggingIn": "登录中…",
|
||||||
"auth.logout": "退出登录",
|
"auth.logout": "退出登录",
|
||||||
"auth.password": "密码",
|
"auth.password": "密码",
|
||||||
"auth.username": "用户名",
|
"auth.username": "用户名",
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,12 @@ async function apiPost(path, body) {
|
||||||
data = null;
|
data = null;
|
||||||
}
|
}
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
|
const isAuthEndpoint =
|
||||||
|
url.includes("/admin/api/auth/login") || url.includes("/admin/api/auth/bootstrap");
|
||||||
|
// Login/bootstrap must never hang on the never-resolving 401 sentinel.
|
||||||
|
if (isAuthEndpoint) {
|
||||||
|
return data && typeof data === "object" ? data : { ok: false, error: "unauthorized" };
|
||||||
|
}
|
||||||
scheduleReauthAfter401(url);
|
scheduleReauthAfter401(url);
|
||||||
return await _haltAfter401();
|
return await _haltAfter401();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export default {
|
||||||
"auth.disabled": "Account is disabled",
|
"auth.disabled": "Account is disabled",
|
||||||
"auth.invalid": "Login failed",
|
"auth.invalid": "Login failed",
|
||||||
"auth.login": "Login",
|
"auth.login": "Login",
|
||||||
|
"auth.loggingIn": "Signing in…",
|
||||||
"auth.logout": "Logout",
|
"auth.logout": "Logout",
|
||||||
"auth.password": "Password",
|
"auth.password": "Password",
|
||||||
"auth.username": "Username",
|
"auth.username": "Username",
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export default {
|
||||||
"auth.disabled": "账号已被禁用,请联系管理员",
|
"auth.disabled": "账号已被禁用,请联系管理员",
|
||||||
"auth.invalid": "登录失败,请检查凭据",
|
"auth.invalid": "登录失败,请检查凭据",
|
||||||
"auth.login": "登录",
|
"auth.login": "登录",
|
||||||
|
"auth.loggingIn": "登录中…",
|
||||||
"auth.logout": "退出登录",
|
"auth.logout": "退出登录",
|
||||||
"auth.password": "密码",
|
"auth.password": "密码",
|
||||||
"auth.username": "用户名",
|
"auth.username": "用户名",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { state, t, el, applyI18nStatic, apiPost, authStoreSet, AUTH_TOKEN_KEY, AUTH_SESSION_KEY, navigateAdmin } from "../core.js";
|
import { state, t, el, mount, applyI18nStatic, apiPost, authStoreSet, AUTH_TOKEN_KEY, AUTH_SESSION_KEY, navigateAdmin } from "../core.js";
|
||||||
|
|
||||||
async function renderLogin() {
|
async function renderLogin() {
|
||||||
applyI18nStatic();
|
applyI18nStatic();
|
||||||
|
|
@ -6,24 +6,51 @@ async function renderLogin() {
|
||||||
const userHint = el("div", { class: "muted", text: t("auth.consoleUsernameHint") });
|
const userHint = el("div", { class: "muted", text: t("auth.consoleUsernameHint") });
|
||||||
const password = el("input", { class: "input", type: "password", placeholder: t("auth.password") });
|
const password = el("input", { class: "input", type: "password", placeholder: t("auth.password") });
|
||||||
const status = el("div", { class: "muted", text: "" });
|
const status = el("div", { class: "muted", text: "" });
|
||||||
const doLogin = async () => {
|
const btn = el("button", { class: "btn btn--primary", type: "button", text: t("auth.login") });
|
||||||
const resp = await apiPost("/admin/api/auth/login", {
|
let busy = false;
|
||||||
tenant_id: "",
|
|
||||||
username: username.value.trim(),
|
const setBusy = (on) => {
|
||||||
password: password.value.trim(),
|
busy = !!on;
|
||||||
purpose: "console",
|
btn.disabled = busy;
|
||||||
});
|
btn.textContent = busy ? t("auth.loggingIn") : t("auth.login");
|
||||||
if (!resp.ok || !resp.token) {
|
|
||||||
const err = String(resp.error || "");
|
|
||||||
status.textContent = err === "user_disabled" ? t("auth.disabled") : t("auth.invalid");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newTok = String(resp.token || "").trim();
|
|
||||||
authStoreSet(AUTH_TOKEN_KEY, newTok);
|
|
||||||
authStoreSet(AUTH_SESSION_KEY, JSON.stringify(resp.session || {}));
|
|
||||||
state.authSession = resp.session || null;
|
|
||||||
await navigateAdmin();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const doLogin = async () => {
|
||||||
|
if (busy) return;
|
||||||
|
setBusy(true);
|
||||||
|
status.textContent = t("auth.loggingIn");
|
||||||
|
try {
|
||||||
|
const resp = await apiPost("/admin/api/auth/login", {
|
||||||
|
tenant_id: "",
|
||||||
|
username: username.value.trim(),
|
||||||
|
password: password.value.trim(),
|
||||||
|
purpose: "console",
|
||||||
|
});
|
||||||
|
if (!resp || !resp.ok || !resp.token) {
|
||||||
|
const err = String((resp && resp.error) || "");
|
||||||
|
status.textContent = err === "user_disabled" ? t("auth.disabled") : t("auth.invalid");
|
||||||
|
setBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newTok = String(resp.token || "").trim();
|
||||||
|
authStoreSet(AUTH_TOKEN_KEY, newTok);
|
||||||
|
authStoreSet(AUTH_SESSION_KEY, JSON.stringify(resp.session || {}));
|
||||||
|
state.authSession = resp.session || null;
|
||||||
|
// Leave the login form immediately so the click feels responsive.
|
||||||
|
mount(
|
||||||
|
el("div", { class: "card" }, [
|
||||||
|
el("div", { class: "card__title", text: t("title.stack") }),
|
||||||
|
el("div", { class: "muted", text: t("chat.loading") }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
||||||
|
await navigateAdmin();
|
||||||
|
} catch (err) {
|
||||||
|
status.textContent = String((err && err.message) || err || t("auth.invalid"));
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onEnterLogin = (ev) => {
|
const onEnterLogin = (ev) => {
|
||||||
if (ev.key !== "Enter") return;
|
if (ev.key !== "Enter") return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
@ -31,7 +58,10 @@ async function renderLogin() {
|
||||||
};
|
};
|
||||||
username.addEventListener("keydown", onEnterLogin);
|
username.addEventListener("keydown", onEnterLogin);
|
||||||
password.addEventListener("keydown", onEnterLogin);
|
password.addEventListener("keydown", onEnterLogin);
|
||||||
const btn = el("button", { class: "btn btn--primary", text: t("auth.login"), onclick: doLogin });
|
btn.addEventListener("click", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
doLogin();
|
||||||
|
});
|
||||||
// Focus after mount so users can type immediately.
|
// Focus after mount so users can type immediately.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -48,5 +78,4 @@ async function renderLogin() {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export { renderLogin };
|
export { renderLogin };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue