mirror of
https://18126008609:longquanjian123@gitee.com/feigong123/aurask.git
synced 2026-04-19 20:03:53 +00:00
665 lines
23 KiB
JavaScript
665 lines
23 KiB
JavaScript
const STORAGE_KEY = "aurask.portal.session";
|
||
const TAB_KEY = "aurask.portal.activeTab";
|
||
const LOCALE_KEY = "aurask.portal.locale";
|
||
const SUPPORTED_LOCALES = ["en", "zh"];
|
||
|
||
const COPY = {
|
||
en: {
|
||
pageTitleSignin: "Aurask | Sign In",
|
||
pageTitleDashboard: "Aurask | Workspace",
|
||
status: {
|
||
api: "API",
|
||
web: "Web",
|
||
devcloud: "DevCloud API Image",
|
||
},
|
||
signin: {
|
||
brand: "Aurask",
|
||
welcomeBack: "Welcome back",
|
||
title: "Sign in to Aurask",
|
||
subtitle: "Continue with Google to access your workspace or create a new one on first sign-in.",
|
||
continueWithGoogle: "Continue with Google",
|
||
helperReady: "Google sign-in is ready. First-time users get a dedicated Aurask workspace automatically.",
|
||
helperLoading: "Google sign-in is loading. Please wait a moment.",
|
||
helperUnavailable: "Google sign-in is unavailable until `AURASK_GOOGLE_CLIENT_ID` is configured.",
|
||
needWorkspace: "Need a first workspace?",
|
||
workspaceAuto: "Aurask provisions one automatically after your first successful sign-in.",
|
||
languageLabel: "Language",
|
||
heroTitle: "Ship AI workflows with private knowledge, safer by default.",
|
||
heroCopy: "Sign in to open your personal workspace, manage Langflow workflows, and use AnythingLLM knowledge bases from one portal.",
|
||
},
|
||
dashboard: {
|
||
workspaceKicker: "Aurask Workspace",
|
||
workspaceSections: "Workspace sections",
|
||
tabs: {
|
||
workflows: "Workflows",
|
||
knowledge: "Knowledge Base",
|
||
},
|
||
profileLabel: "Profile",
|
||
signOut: "Sign out",
|
||
personalCenter: "Personal Center",
|
||
userFallback: "Aurask User",
|
||
deployDefaults: "Deploy Defaults",
|
||
tenant: "Tenant",
|
||
workspace: "Workspace",
|
||
plan: "Plan",
|
||
tbu: "TBU",
|
||
knowledgeBases: "Knowledge Bases",
|
||
tenantId: "Tenant ID",
|
||
workspaceId: "Workspace ID",
|
||
apiImage: "API Image",
|
||
webImage: "Web Image",
|
||
apiGateway: "API Gateway",
|
||
langflowEmbed: "Langflow Embed",
|
||
anythingllmEmbed: "AnythingLLM Embed",
|
||
workflowStudio: "Workflow Studio",
|
||
langflow: "Langflow",
|
||
knowledgeBaseTitle: "Knowledge Base",
|
||
anythingllm: "AnythingLLM",
|
||
openNewTab: "Open in new tab",
|
||
plans: {
|
||
free_trial: "Free Trial",
|
||
basic_monthly: "Basic",
|
||
dedicated_space: "Dedicated Space",
|
||
},
|
||
},
|
||
messages: {
|
||
loading: "Loading Aurask...",
|
||
sessionExpired: "Your Aurask session has expired. Please sign in again.",
|
||
googleSignInFailed: "Google sign-in failed",
|
||
configLoadFailed: "Failed to load Aurask configuration",
|
||
},
|
||
},
|
||
zh: {
|
||
pageTitleSignin: "Aurask | 登录",
|
||
pageTitleDashboard: "Aurask | 工作台",
|
||
status: {
|
||
api: "接口",
|
||
web: "站点",
|
||
devcloud: "DevCloud API 镜像",
|
||
},
|
||
signin: {
|
||
brand: "Aurask",
|
||
welcomeBack: "欢迎回来",
|
||
title: "登录 Aurask",
|
||
subtitle: "使用 Google 登录访问你的工作空间;首次登录时会自动创建专属 workspace。",
|
||
continueWithGoogle: "使用 Google 继续",
|
||
helperReady: "Google 登录已就绪,首次登录的用户会自动开通独立 Aurask workspace。",
|
||
helperLoading: "Google 登录组件正在加载,请稍候。",
|
||
helperUnavailable: "未配置 `AURASK_GOOGLE_CLIENT_ID` 前,Google 登录按钮会保持灰色禁用状态。",
|
||
needWorkspace: "还没有 workspace?",
|
||
workspaceAuto: "首次成功登录后,Aurask 会自动为你创建一个。",
|
||
languageLabel: "语言",
|
||
heroTitle: "以更稳妥的方式交付连接私有知识的 AI 工作流。",
|
||
heroCopy: "登录后即可进入你的个人 workspace,在同一门户中管理 Langflow 工作流并使用 AnythingLLM 知识库。",
|
||
},
|
||
dashboard: {
|
||
workspaceKicker: "Aurask 工作台",
|
||
workspaceSections: "工作区导航",
|
||
tabs: {
|
||
workflows: "工作流",
|
||
knowledge: "知识库",
|
||
},
|
||
profileLabel: "个人中心",
|
||
signOut: "退出登录",
|
||
personalCenter: "个人中心",
|
||
userFallback: "Aurask 用户",
|
||
deployDefaults: "部署信息",
|
||
tenant: "租户",
|
||
workspace: "工作空间",
|
||
plan: "套餐",
|
||
tbu: "TBU",
|
||
knowledgeBases: "知识库数量",
|
||
tenantId: "租户 ID",
|
||
workspaceId: "工作空间 ID",
|
||
apiImage: "API 镜像",
|
||
webImage: "Web 镜像",
|
||
apiGateway: "API 网关",
|
||
langflowEmbed: "Langflow 嵌入地址",
|
||
anythingllmEmbed: "AnythingLLM 嵌入地址",
|
||
workflowStudio: "工作流工作台",
|
||
langflow: "Langflow",
|
||
knowledgeBaseTitle: "知识库",
|
||
anythingllm: "AnythingLLM",
|
||
openNewTab: "新标签页打开",
|
||
plans: {
|
||
free_trial: "免费试用",
|
||
basic_monthly: "基础版",
|
||
dedicated_space: "独享空间",
|
||
},
|
||
},
|
||
messages: {
|
||
loading: "Aurask 加载中...",
|
||
sessionExpired: "Aurask 会话已过期,请重新登录。",
|
||
googleSignInFailed: "Google 登录失败",
|
||
configLoadFailed: "Aurask 配置加载失败",
|
||
},
|
||
},
|
||
};
|
||
|
||
const app = document.querySelector("#app");
|
||
|
||
const state = {
|
||
config: null,
|
||
sessionToken: window.localStorage.getItem(STORAGE_KEY) || "",
|
||
profile: null,
|
||
activeTab: window.localStorage.getItem(TAB_KEY) || "workflows",
|
||
locale: detectInitialLocale(),
|
||
googleSdkReady: Boolean(window.google?.accounts?.id),
|
||
loading: true,
|
||
errorKey: "",
|
||
errorDetail: "",
|
||
};
|
||
|
||
applyDocumentLanguage();
|
||
|
||
function normalizeLocale(locale) {
|
||
return SUPPORTED_LOCALES.includes(locale) ? locale : null;
|
||
}
|
||
|
||
function detectInitialLocale() {
|
||
const storedLocale = normalizeLocale(window.localStorage.getItem(LOCALE_KEY));
|
||
if (storedLocale) return storedLocale;
|
||
return String(window.navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en";
|
||
}
|
||
|
||
function persistLocale(locale) {
|
||
state.locale = normalizeLocale(locale) || "en";
|
||
window.localStorage.setItem(LOCALE_KEY, state.locale);
|
||
applyDocumentLanguage();
|
||
}
|
||
|
||
function applyDocumentLanguage() {
|
||
document.documentElement.lang = state.locale === "zh" ? "zh-CN" : "en";
|
||
}
|
||
|
||
function copy(path) {
|
||
return path.split(".").reduce((value, part) => value?.[part], COPY[state.locale]) ?? path;
|
||
}
|
||
|
||
function setDocumentTitle() {
|
||
document.title = state.profile ? copy("pageTitleDashboard") : copy("pageTitleSignin");
|
||
}
|
||
|
||
function setLocale(locale) {
|
||
const nextLocale = normalizeLocale(locale);
|
||
if (!nextLocale || nextLocale === state.locale) return;
|
||
persistLocale(nextLocale);
|
||
render();
|
||
if (!state.profile) {
|
||
mountGoogleButton();
|
||
}
|
||
}
|
||
|
||
function setError(key, detail = "") {
|
||
state.errorKey = key;
|
||
state.errorDetail = detail || "";
|
||
}
|
||
|
||
function clearError() {
|
||
state.errorKey = "";
|
||
state.errorDetail = "";
|
||
}
|
||
|
||
function renderError() {
|
||
if (!state.errorKey) return "";
|
||
const message = copy(state.errorKey);
|
||
if (!state.errorDetail) return escapeHtml(message);
|
||
return `${escapeHtml(message)}: ${escapeHtml(state.errorDetail)}`;
|
||
}
|
||
|
||
function detectApiBase() {
|
||
if (window.location.protocol === "file:") return "http://127.0.0.1:8080";
|
||
if (["127.0.0.1", "localhost"].includes(window.location.hostname)) return "http://127.0.0.1:8080";
|
||
return `${window.location.origin}/api`;
|
||
}
|
||
|
||
function normalizeApiBase(url) {
|
||
return (url || detectApiBase()).replace(/\/$/, "");
|
||
}
|
||
|
||
async function request(path, options = {}) {
|
||
const headers = new Headers(options.headers || {});
|
||
headers.set("Accept", "application/json");
|
||
if (options.body !== undefined) headers.set("Content-Type", "application/json");
|
||
if (state.sessionToken) headers.set("Authorization", `Bearer ${state.sessionToken}`);
|
||
|
||
const response = await fetch(`${normalizeApiBase(state.config?.public_api_base_url)}${path}`, {
|
||
...options,
|
||
headers,
|
||
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
||
});
|
||
const payload = await response.json().catch(() => ({}));
|
||
if (!response.ok) {
|
||
const message = payload?.error?.message || payload?.message || "Request failed";
|
||
const error = new Error(message);
|
||
error.status = response.status;
|
||
error.payload = payload;
|
||
throw error;
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
function persistSession(token) {
|
||
state.sessionToken = token || "";
|
||
if (state.sessionToken) {
|
||
window.localStorage.setItem(STORAGE_KEY, state.sessionToken);
|
||
} else {
|
||
window.localStorage.removeItem(STORAGE_KEY);
|
||
}
|
||
}
|
||
|
||
function setTab(tab) {
|
||
state.activeTab = tab;
|
||
window.localStorage.setItem(TAB_KEY, tab);
|
||
render();
|
||
}
|
||
|
||
function initials(label) {
|
||
return (label || "AU")
|
||
.split(/\s+/)
|
||
.filter(Boolean)
|
||
.slice(0, 2)
|
||
.map((part) => part[0]?.toUpperCase())
|
||
.join("");
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value || "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function withEmbedContext(baseUrl) {
|
||
if (!baseUrl) return "";
|
||
const url = new URL(baseUrl, window.location.origin);
|
||
if (state.profile?.tenant?.id) url.searchParams.set("tenant_id", state.profile.tenant.id);
|
||
if (state.profile?.workspace?.id) url.searchParams.set("workspace_id", state.profile.workspace.id);
|
||
url.searchParams.set("source", "aurask-protal");
|
||
return url.toString();
|
||
}
|
||
|
||
function ensureSigninRoute() {
|
||
if (state.profile) return;
|
||
if (window.location.pathname !== "/signin") {
|
||
window.history.replaceState({}, "", "/signin");
|
||
}
|
||
}
|
||
|
||
function ensureDashboardRoute() {
|
||
if (!state.profile) return;
|
||
if (!window.location.pathname.startsWith("/app")) {
|
||
window.history.replaceState({}, "", "/app");
|
||
}
|
||
}
|
||
|
||
function googleStatus() {
|
||
const googleConfig = state.config?.auth?.google || {};
|
||
if (!googleConfig.enabled || !googleConfig.client_id) return "unavailable";
|
||
if (!state.googleSdkReady) return "loading";
|
||
return "ready";
|
||
}
|
||
|
||
function renderStatusPills() {
|
||
if (!state.config) return "";
|
||
return `
|
||
<div class="pill-row">
|
||
<span class="pill">${escapeHtml(copy("status.api"))}: ${escapeHtml(state.config.public_api_base_url)}</span>
|
||
<span class="pill">${escapeHtml(copy("status.web"))}: ${escapeHtml(state.config.public_base_url)}</span>
|
||
<span class="pill">${escapeHtml(copy("status.devcloud"))}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderLocaleSwitcher() {
|
||
return `
|
||
<div class="locale-switcher" aria-label="${escapeHtml(copy("signin.languageLabel"))}">
|
||
<button class="locale-btn ${state.locale === "en" ? "is-active" : ""}" data-locale="en" type="button" lang="en" aria-pressed="${state.locale === "en"}">EN</button>
|
||
<button class="locale-btn ${state.locale === "zh" ? "is-active" : ""}" data-locale="zh" type="button" lang="zh-CN" aria-pressed="${state.locale === "zh"}">中文</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderSigninGoogleArea() {
|
||
if (googleStatus() === "ready") {
|
||
return `
|
||
<div class="google-slot">
|
||
<div id="googleButton" class="google-box"></div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return `
|
||
<div class="google-slot">
|
||
<button class="auth-btn auth-btn-secondary auth-btn-fallback" type="button" disabled aria-disabled="true">${escapeHtml(copy("signin.continueWithGoogle"))}</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderSigninHelper() {
|
||
const status = googleStatus();
|
||
if (status === "unavailable") return copy("signin.helperUnavailable");
|
||
if (status === "loading") return copy("signin.helperLoading");
|
||
return copy("signin.helperReady");
|
||
}
|
||
|
||
function formatPlanName(planCode) {
|
||
return copy(`dashboard.plans.${planCode}`) === `dashboard.plans.${planCode}`
|
||
? planCode || ""
|
||
: copy(`dashboard.plans.${planCode}`);
|
||
}
|
||
|
||
function renderSignin() {
|
||
app.innerHTML = `
|
||
<main class="signin-layout">
|
||
<section class="signin-brand">
|
||
<div class="brand-mark">A</div>
|
||
<p class="brand-eyebrow">${escapeHtml(copy("signin.brand"))}</p>
|
||
<h1>${escapeHtml(copy("signin.heroTitle"))}</h1>
|
||
<p class="brand-copy">${escapeHtml(copy("signin.heroCopy"))}</p>
|
||
${renderStatusPills()}
|
||
</section>
|
||
<section class="signin-card">
|
||
<div class="signin-header">
|
||
<div class="signin-header-top">
|
||
<p class="section-kicker">${escapeHtml(copy("signin.welcomeBack"))}</p>
|
||
${renderLocaleSwitcher()}
|
||
</div>
|
||
<h2>${escapeHtml(copy("signin.title"))}</h2>
|
||
<p>${escapeHtml(copy("signin.subtitle"))}</p>
|
||
</div>
|
||
${renderSigninGoogleArea()}
|
||
<p class="signin-tip">${escapeHtml(renderSigninHelper())}</p>
|
||
${state.errorKey ? `<p class="message error">${renderError()}</p>` : ""}
|
||
<div class="signin-footer">
|
||
<span>${escapeHtml(copy("signin.needWorkspace"))}</span>
|
||
<span>${escapeHtml(copy("signin.workspaceAuto"))}</span>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
`;
|
||
|
||
document.querySelectorAll("[data-locale]").forEach((button) => {
|
||
button.addEventListener("click", () => setLocale(button.dataset.locale));
|
||
});
|
||
}
|
||
|
||
function renderDashboard() {
|
||
const user = state.profile?.user || {};
|
||
const tenant = state.profile?.tenant || {};
|
||
const workspace = state.profile?.workspace || {};
|
||
const quota = state.profile?.quota || {};
|
||
const config = state.profile?.config || state.config || {};
|
||
const langflowUrl = withEmbedContext(config.embeds?.langflow_url);
|
||
const anythingllmUrl = withEmbedContext(config.embeds?.anythingllm_url);
|
||
|
||
app.innerHTML = `
|
||
<main class="dashboard-shell">
|
||
<header class="topbar">
|
||
<div class="topbar-left">
|
||
<div class="brand-mark brand-mark-small">A</div>
|
||
<div>
|
||
<p class="section-kicker">${escapeHtml(copy("dashboard.workspaceKicker"))}</p>
|
||
<h1>${escapeHtml(tenant.name || "Aurask")}</h1>
|
||
</div>
|
||
</div>
|
||
<div class="topbar-right">
|
||
<nav class="tabbar" aria-label="${escapeHtml(copy("dashboard.workspaceSections"))}">
|
||
<button class="tab ${state.activeTab === "workflows" ? "is-active" : ""}" data-tab="workflows" type="button">${escapeHtml(copy("dashboard.tabs.workflows"))}</button>
|
||
<button class="tab ${state.activeTab === "knowledge" ? "is-active" : ""}" data-tab="knowledge" type="button">${escapeHtml(copy("dashboard.tabs.knowledge"))}</button>
|
||
</nav>
|
||
<details class="profile-menu">
|
||
<summary>
|
||
${
|
||
user.avatar_url
|
||
? `<img class="avatar-image" src="${escapeHtml(user.avatar_url)}" alt="${escapeHtml(user.display_name || user.email || copy("dashboard.profileLabel"))}" />`
|
||
: `<span class="avatar">${escapeHtml(initials(user.display_name || user.email))}</span>`
|
||
}
|
||
<span class="profile-name">${escapeHtml(user.display_name || user.email || copy("dashboard.profileLabel"))}</span>
|
||
</summary>
|
||
<div class="profile-panel">
|
||
<div class="profile-block">
|
||
<strong>${escapeHtml(user.display_name || user.email || "")}</strong>
|
||
<span>${escapeHtml(user.email || "")}</span>
|
||
</div>
|
||
<div class="profile-block">
|
||
<span>${escapeHtml(copy("dashboard.tenant"))}</span>
|
||
<strong>${escapeHtml(tenant.name || "")}</strong>
|
||
</div>
|
||
<div class="profile-block">
|
||
<span>${escapeHtml(copy("dashboard.workspace"))}</span>
|
||
<strong>${escapeHtml(workspace.name || "")}</strong>
|
||
</div>
|
||
<div class="profile-block profile-grid">
|
||
<span>${escapeHtml(copy("dashboard.plan"))}</span>
|
||
<strong>${escapeHtml(formatPlanName(quota.plan_code))}</strong>
|
||
<span>${escapeHtml(copy("dashboard.tbu"))}</span>
|
||
<strong>${escapeHtml(String(quota.available_tbu ?? 0))}</strong>
|
||
<span>${escapeHtml(copy("dashboard.knowledgeBases"))}</span>
|
||
<strong>${escapeHtml(String(quota.knowledge_bases ?? 0))}</strong>
|
||
</div>
|
||
<button id="logoutButton" class="auth-btn auth-btn-secondary" type="button">${escapeHtml(copy("dashboard.signOut"))}</button>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</header>
|
||
<section class="dashboard-grid">
|
||
<aside class="side-panel">
|
||
<section class="info-card">
|
||
<p class="section-kicker">${escapeHtml(copy("dashboard.personalCenter"))}</p>
|
||
<h2>${escapeHtml(user.display_name || user.email || copy("dashboard.userFallback"))}</h2>
|
||
<p>${escapeHtml(user.email || "")}</p>
|
||
<div class="stat-list">
|
||
<div class="stat-item">
|
||
<span>${escapeHtml(copy("dashboard.tenantId"))}</span>
|
||
<strong>${escapeHtml(tenant.id || "")}</strong>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span>${escapeHtml(copy("dashboard.workspaceId"))}</span>
|
||
<strong>${escapeHtml(workspace.id || "")}</strong>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span>${escapeHtml(copy("dashboard.apiImage"))}</span>
|
||
<strong>${escapeHtml(config.devcloud?.api_image || "")}</strong>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span>${escapeHtml(copy("dashboard.webImage"))}</span>
|
||
<strong>${escapeHtml(config.devcloud?.web_image || "")}</strong>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="info-card">
|
||
<p class="section-kicker">${escapeHtml(copy("dashboard.deployDefaults"))}</p>
|
||
<ul class="plain-list">
|
||
<li>${escapeHtml(copy("dashboard.apiGateway"))}: ${escapeHtml(config.public_api_base_url || "")}</li>
|
||
<li>${escapeHtml(copy("dashboard.langflowEmbed"))}: ${escapeHtml(config.embeds?.langflow_url || "")}</li>
|
||
<li>${escapeHtml(copy("dashboard.anythingllmEmbed"))}: ${escapeHtml(config.embeds?.anythingllm_url || "")}</li>
|
||
</ul>
|
||
</section>
|
||
</aside>
|
||
<section class="workspace-panel">
|
||
<div class="embed-card ${state.activeTab === "workflows" ? "" : "is-hidden"}">
|
||
<div class="embed-header">
|
||
<div>
|
||
<p class="section-kicker">${escapeHtml(copy("dashboard.workflowStudio"))}</p>
|
||
<h2>${escapeHtml(copy("dashboard.langflow"))}</h2>
|
||
</div>
|
||
<a href="${escapeHtml(langflowUrl)}" target="_blank" rel="noreferrer">${escapeHtml(copy("dashboard.openNewTab"))}</a>
|
||
</div>
|
||
<iframe title="${escapeHtml(copy("dashboard.langflow"))}" src="${escapeHtml(langflowUrl)}"></iframe>
|
||
</div>
|
||
<div class="embed-card ${state.activeTab === "knowledge" ? "" : "is-hidden"}">
|
||
<div class="embed-header">
|
||
<div>
|
||
<p class="section-kicker">${escapeHtml(copy("dashboard.knowledgeBaseTitle"))}</p>
|
||
<h2>${escapeHtml(copy("dashboard.anythingllm"))}</h2>
|
||
</div>
|
||
<a href="${escapeHtml(anythingllmUrl)}" target="_blank" rel="noreferrer">${escapeHtml(copy("dashboard.openNewTab"))}</a>
|
||
</div>
|
||
<iframe title="${escapeHtml(copy("dashboard.anythingllm"))}" src="${escapeHtml(anythingllmUrl)}"></iframe>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
${state.errorKey ? `<div class="toast error">${renderError()}</div>` : ""}
|
||
</main>
|
||
`;
|
||
|
||
document.querySelectorAll("[data-tab]").forEach((button) => {
|
||
button.addEventListener("click", () => setTab(button.dataset.tab));
|
||
});
|
||
document.querySelector("#logoutButton")?.addEventListener("click", logout);
|
||
}
|
||
|
||
function renderLoading() {
|
||
app.innerHTML = `
|
||
<main class="loading-shell">
|
||
<div class="loader"></div>
|
||
<p>${escapeHtml(copy("messages.loading"))}</p>
|
||
</main>
|
||
`;
|
||
}
|
||
|
||
function render() {
|
||
applyDocumentLanguage();
|
||
setDocumentTitle();
|
||
|
||
if (state.loading) {
|
||
renderLoading();
|
||
return;
|
||
}
|
||
|
||
if (state.profile) {
|
||
renderDashboard();
|
||
return;
|
||
}
|
||
|
||
renderSignin();
|
||
}
|
||
|
||
async function bootstrap() {
|
||
state.loading = true;
|
||
clearError();
|
||
render();
|
||
|
||
try {
|
||
const activeApiBase = detectApiBase();
|
||
const localHost =
|
||
window.location.protocol === "file:" || ["127.0.0.1", "localhost"].includes(window.location.hostname);
|
||
|
||
state.config = await fetch(`${activeApiBase}/auth/config`).then((response) => response.json());
|
||
state.config.public_api_base_url = normalizeApiBase(
|
||
localHost ? activeApiBase : state.config.public_api_base_url || activeApiBase,
|
||
);
|
||
state.googleSdkReady = state.googleSdkReady || Boolean(window.google?.accounts?.id);
|
||
|
||
if (state.sessionToken) {
|
||
try {
|
||
state.profile = await request("/auth/session");
|
||
ensureDashboardRoute();
|
||
} catch (_error) {
|
||
persistSession("");
|
||
state.profile = null;
|
||
}
|
||
}
|
||
|
||
if (!state.profile) {
|
||
ensureSigninRoute();
|
||
}
|
||
} catch (error) {
|
||
setError("messages.configLoadFailed", error.message || "");
|
||
} finally {
|
||
state.loading = false;
|
||
render();
|
||
mountGoogleButton();
|
||
}
|
||
}
|
||
|
||
async function signInWithGoogle(idToken) {
|
||
clearError();
|
||
render();
|
||
|
||
try {
|
||
const payload = await request("/auth/google/login", {
|
||
method: "POST",
|
||
body: { id_token: idToken },
|
||
});
|
||
persistSession(payload.token);
|
||
state.profile = payload;
|
||
ensureDashboardRoute();
|
||
render();
|
||
} catch (error) {
|
||
setError("messages.googleSignInFailed", error.message || "");
|
||
render();
|
||
mountGoogleButton();
|
||
}
|
||
}
|
||
|
||
async function logout() {
|
||
try {
|
||
await request("/auth/logout", { method: "POST" });
|
||
} catch (error) {
|
||
console.warn("Logout request failed", error);
|
||
}
|
||
|
||
persistSession("");
|
||
state.profile = null;
|
||
ensureSigninRoute();
|
||
render();
|
||
mountGoogleButton();
|
||
}
|
||
|
||
function mountGoogleButton() {
|
||
const googleBox = document.querySelector("#googleButton");
|
||
if (!googleBox || googleStatus() !== "ready" || !window.google?.accounts?.id) return;
|
||
|
||
googleBox.innerHTML = "";
|
||
window.google.accounts.id.initialize({
|
||
client_id: state.config.auth.google.client_id,
|
||
callback: ({ credential }) => signInWithGoogle(credential),
|
||
});
|
||
|
||
const width = Math.max(280, Math.floor(googleBox.getBoundingClientRect().width || 320));
|
||
window.google.accounts.id.renderButton(googleBox, {
|
||
theme: "outline",
|
||
size: "large",
|
||
shape: "pill",
|
||
text: "continue_with",
|
||
width,
|
||
});
|
||
}
|
||
|
||
window.addEventListener("popstate", render);
|
||
|
||
document.addEventListener("visibilitychange", async () => {
|
||
if (document.visibilityState !== "visible" || !state.profile || !state.sessionToken) return;
|
||
|
||
try {
|
||
state.profile = await request("/auth/session");
|
||
render();
|
||
} catch (_error) {
|
||
persistSession("");
|
||
state.profile = null;
|
||
setError("messages.sessionExpired");
|
||
ensureSigninRoute();
|
||
render();
|
||
mountGoogleButton();
|
||
}
|
||
});
|
||
|
||
const googleScript = document.createElement("script");
|
||
googleScript.src = "https://accounts.google.com/gsi/client";
|
||
googleScript.async = true;
|
||
googleScript.defer = true;
|
||
googleScript.onload = () => {
|
||
state.googleSdkReady = Boolean(window.google?.accounts?.id);
|
||
if (!state.profile && !state.loading) {
|
||
render();
|
||
mountGoogleButton();
|
||
}
|
||
};
|
||
document.head.appendChild(googleScript);
|
||
|
||
bootstrap();
|