mirror of
https://18126008609:longquanjian123@gitee.com/feigong123/aurask.git
synced 2026-04-19 21:50:35 +00:00
feat: add bilingual portal signin flow
All checks were successful
aurask-release / build-and-deploy (push) Successful in 1m56s
All checks were successful
aurask-release / build-and-deploy (push) Successful in 1m56s
This commit is contained in:
parent
4e2639ea43
commit
c2fc63f5f1
37
README.md
37
README.md
@ -1,20 +1,26 @@
|
|||||||
## Aurask
|
## Aurask
|
||||||
|
|
||||||
Aurask 当前是一个按产品边界拆分的首版实现,根目录保持为四个主要目录:
|
Aurask 当前按产品边界拆分为四个主要目录:
|
||||||
|
|
||||||
- `api/`:后端网关、会话登录、配额/TBU、工作流、知识库桥接
|
- `api/`:后端 API、会话登录、套餐 TBU、工作流与知识库桥接
|
||||||
- `protal/`:用户门户,保留既定目录拼写
|
- `protal/`:用户门户,保留既定目录拼写
|
||||||
- `manager/`:管理员面板
|
- `manager/`:管理员面板
|
||||||
- `deploy/`:k3s 与 DevCloud 部署配置
|
- `deploy/`:k3s 与 DevCloud 部署配置
|
||||||
|
|
||||||
当前版本已经补齐了用户门户登录闭环:
|
## 当前实现范围
|
||||||
|
|
||||||
- `/signin` 风格化登录页
|
- `/signin` 登录页
|
||||||
- Google 首次注册/登录后自动创建独立 workspace
|
- Google 首次注册 / 登录后自动创建独立 workspace
|
||||||
- 登录后双标签页工作台:
|
- 登录后双标签工作台:
|
||||||
- `Workflows` 内嵌 `Langflow`
|
- `Workflows` 内嵌 `Langflow`
|
||||||
- `Knowledge Base` 内嵌 `AnythingLLM`
|
- `Knowledge Base` 内嵌 `AnythingLLM`
|
||||||
- 右上角个人中心与登出
|
- 右上角个人中心与退出登录
|
||||||
|
- 门户双语支持:
|
||||||
|
- `/signin` 提供 `EN / 中文` 语言切换
|
||||||
|
- `/app` 工作台壳层跟随已保存语言
|
||||||
|
- 首次访问默认读取浏览器语言
|
||||||
|
- 用户选择写入 `localStorage`
|
||||||
|
- Google 登录按钮默认灰色禁用,只有在配置完成且 Google SDK 就绪后才替换为真实按钮
|
||||||
|
|
||||||
## 本地运行
|
## 本地运行
|
||||||
|
|
||||||
@ -25,14 +31,14 @@ $env:PYTHONPATH='api'
|
|||||||
py -3 -m aurask serve --reset --host 127.0.0.1 --port 8080
|
py -3 -m aurask serve --reset --host 127.0.0.1 --port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
运行演示:
|
运行演示数据:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:PYTHONPATH='api'
|
$env:PYTHONPATH='api'
|
||||||
py -3 -m aurask demo --reset
|
py -3 -m aurask demo --reset
|
||||||
```
|
```
|
||||||
|
|
||||||
门户静态文件位于 `protal/`,部署到静态 Web 服务后即可使用。开发时可直接由本地静态服务器打开 `protal/index.html`。
|
门户静态文件位于 `protal/`,部署到静态 Web 服务后即可使用。开发时可直接通过本地静态服务器打开 `protal/index.html`。
|
||||||
|
|
||||||
## 当前接口
|
## 当前接口
|
||||||
|
|
||||||
@ -61,9 +67,9 @@ py -3 -m aurask demo --reset
|
|||||||
- `GET /workflow-runs/{run_id}`
|
- `GET /workflow-runs/{run_id}`
|
||||||
- `GET /admin/bridge-status`
|
- `GET /admin/bridge-status`
|
||||||
|
|
||||||
Aurask 现在同时支持两类 Bearer Token:
|
Aurask 目前同时支持两类 Bearer Token:
|
||||||
|
|
||||||
- API Key:用于原有 API/集成访问
|
- API Key:用于原生 API / 集成访问
|
||||||
- Session Token:用于门户登录态
|
- Session Token:用于门户登录态
|
||||||
|
|
||||||
## 关键环境变量
|
## 关键环境变量
|
||||||
@ -80,6 +86,12 @@ AURASK_GOOGLE_CLIENT_ID=<google-client-id>
|
|||||||
AURASK_SESSION_TTL_DAYS=7
|
AURASK_SESSION_TTL_DAYS=7
|
||||||
```
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 未设置 `AURASK_GOOGLE_CLIENT_ID` 时,`/signin` 会显示灰色禁用的 Google 按钮
|
||||||
|
- 当 `auth.google.enabled` 为真、`client_id` 存在且 Google SDK 加载完成后,前端才会挂载真实按钮
|
||||||
|
- 语言偏好保存在浏览器 `localStorage` 的 `aurask.portal.locale`
|
||||||
|
|
||||||
运行时桥接:
|
运行时桥接:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@ -115,11 +127,12 @@ $env:PYTHONPATH='api'
|
|||||||
cmd /c "py -3 -m unittest discover -s tests -v"
|
cmd /c "py -3 -m unittest discover -s tests -v"
|
||||||
```
|
```
|
||||||
|
|
||||||
当前已覆盖:
|
当前覆盖:
|
||||||
|
|
||||||
- MVP 业务闭环
|
- MVP 业务闭环
|
||||||
- PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接契约
|
- PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接契约
|
||||||
- Google 登录与 session
|
- Google 登录与 session
|
||||||
|
- 门户双语登录与工作台壳层
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
|
|||||||
@ -1,34 +1,50 @@
|
|||||||
# Aurask Protal
|
# Aurask Protal
|
||||||
|
|
||||||
`protal/` 是用户门户目录,目录名按既定要求保留为 `protal`。
|
`protal/` 是 Aurask 用户门户目录,目录名按既定要求保留为 `protal`。
|
||||||
|
|
||||||
## 当前页面能力
|
## 当前能力
|
||||||
|
|
||||||
- 未登录时渲染 `/signin` 风格登录页
|
- 未登录用户访问 `/signin` 时展示 Aurask 自有登录页
|
||||||
- 支持 Google 登录入口
|
- 登录卡片右上角支持 `EN / 中文` 语言切换
|
||||||
- 新用户首次登录自动创建独立 workspace
|
- 首次访问默认读取浏览器语言:
|
||||||
- 登录后显示:
|
- `navigator.language` 以 `zh` 开头时默认中文
|
||||||
- `Workflows`:内嵌 Langflow
|
- 其他情况默认英文
|
||||||
- `Knowledge Base`:内嵌 AnythingLLM
|
- 用户手动切换语言后会写入 `localStorage`
|
||||||
- 右上角个人中心展示用户、租户、workspace 与基础额度信息
|
- 语言范围覆盖 Aurask 自有门户界面:
|
||||||
|
- `/signin`
|
||||||
|
- 登录后的 `/app` 工作台壳层
|
||||||
|
- 嵌入的 `Langflow` / `AnythingLLM` 保持其自身语言,不做翻译
|
||||||
|
|
||||||
|
## Google 登录行为
|
||||||
|
|
||||||
|
- 门户继续使用 `GET /auth/config` 返回的 Google 配置
|
||||||
|
- 只有同时满足以下条件时才会挂载真实 Google 按钮:
|
||||||
|
- `auth.google.enabled === true`
|
||||||
|
- `auth.google.client_id` 有效
|
||||||
|
- Google GSI SDK 已加载完成
|
||||||
|
- 在配置缺失或 SDK 未就绪前,页面始终展示灰色禁用按钮
|
||||||
|
- 禁用按钮不可点击,并通过辅助文案提示当前状态
|
||||||
|
- 新用户首次成功登录后,后端仍会自动创建独立 workspace
|
||||||
|
|
||||||
|
## 本地存储键
|
||||||
|
|
||||||
|
- `aurask.portal.session`:登录会话令牌
|
||||||
|
- `aurask.portal.activeTab`:当前工作台标签
|
||||||
|
- `aurask.portal.locale`:门户语言选择
|
||||||
|
|
||||||
## 依赖接口
|
## 依赖接口
|
||||||
|
|
||||||
门户依赖以下 API:
|
|
||||||
|
|
||||||
- `GET /auth/config`
|
- `GET /auth/config`
|
||||||
- `POST /auth/google/login`
|
- `POST /auth/google/login`
|
||||||
- `GET /auth/session`
|
- `GET /auth/session`
|
||||||
- `POST /auth/logout`
|
- `POST /auth/logout`
|
||||||
|
|
||||||
## 部署说明
|
## 部署假设
|
||||||
|
|
||||||
门户默认假设:
|
- 生产门户地址:`https://aurask.xyz`
|
||||||
|
- 生产 API 地址:`https://aurask.xyz/api`
|
||||||
- 生产环境通过 `https://aurask.xyz`
|
- Langflow iframe:`https://aurask.xyz/runtime/langflow/`
|
||||||
- API 通过 `https://aurask.xyz/api`
|
- AnythingLLM iframe:`https://aurask.xyz/runtime/anythingllm/`
|
||||||
- Langflow iframe 通过 `https://aurask.xyz/runtime/langflow/`
|
|
||||||
- AnythingLLM iframe 通过 `https://aurask.xyz/runtime/anythingllm/`
|
|
||||||
|
|
||||||
本地调试时会自动回退到:
|
本地调试时会自动回退到:
|
||||||
|
|
||||||
|
|||||||
564
protal/main.js
564
protal/main.js
@ -1,5 +1,140 @@
|
|||||||
const STORAGE_KEY = "aurask.portal.session";
|
const STORAGE_KEY = "aurask.portal.session";
|
||||||
const TAB_KEY = "aurask.portal.activeTab";
|
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 app = document.querySelector("#app");
|
||||||
|
|
||||||
@ -8,10 +143,70 @@ const state = {
|
|||||||
sessionToken: window.localStorage.getItem(STORAGE_KEY) || "",
|
sessionToken: window.localStorage.getItem(STORAGE_KEY) || "",
|
||||||
profile: null,
|
profile: null,
|
||||||
activeTab: window.localStorage.getItem(TAB_KEY) || "workflows",
|
activeTab: window.localStorage.getItem(TAB_KEY) || "workflows",
|
||||||
|
locale: detectInitialLocale(),
|
||||||
|
googleSdkReady: Boolean(window.google?.accounts?.id),
|
||||||
loading: true,
|
loading: true,
|
||||||
error: "",
|
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() {
|
function detectApiBase() {
|
||||||
if (window.location.protocol === "file:") return "http://127.0.0.1:8080";
|
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";
|
if (["127.0.0.1", "localhost"].includes(window.location.hostname)) return "http://127.0.0.1:8080";
|
||||||
@ -27,6 +222,7 @@ async function request(path, options = {}) {
|
|||||||
headers.set("Accept", "application/json");
|
headers.set("Accept", "application/json");
|
||||||
if (options.body !== undefined) headers.set("Content-Type", "application/json");
|
if (options.body !== undefined) headers.set("Content-Type", "application/json");
|
||||||
if (state.sessionToken) headers.set("Authorization", `Bearer ${state.sessionToken}`);
|
if (state.sessionToken) headers.set("Authorization", `Bearer ${state.sessionToken}`);
|
||||||
|
|
||||||
const response = await fetch(`${normalizeApiBase(state.config?.public_api_base_url)}${path}`, {
|
const response = await fetch(`${normalizeApiBase(state.config?.public_api_base_url)}${path}`, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
@ -99,126 +295,95 @@ function ensureDashboardRoute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
function googleStatus() {
|
||||||
state.loading = true;
|
const googleConfig = state.config?.auth?.google || {};
|
||||||
render();
|
if (!googleConfig.enabled || !googleConfig.client_id) return "unavailable";
|
||||||
try {
|
if (!state.googleSdkReady) return "loading";
|
||||||
const activeApiBase = detectApiBase();
|
return "ready";
|
||||||
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);
|
|
||||||
if (state.sessionToken) {
|
|
||||||
try {
|
|
||||||
state.profile = await request("/auth/session");
|
|
||||||
ensureDashboardRoute();
|
|
||||||
} catch (error) {
|
|
||||||
persistSession("");
|
|
||||||
state.profile = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!state.profile) ensureSigninRoute();
|
|
||||||
} catch (error) {
|
|
||||||
state.error = error.message || "Failed to load Aurask configuration";
|
|
||||||
} finally {
|
|
||||||
state.loading = false;
|
|
||||||
render();
|
|
||||||
mountGoogleButton();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function signInWithGoogle(idToken) {
|
|
||||||
state.error = "";
|
|
||||||
render();
|
|
||||||
try {
|
|
||||||
const payload = await request("/auth/google/login", {
|
|
||||||
method: "POST",
|
|
||||||
body: { id_token: idToken },
|
|
||||||
});
|
|
||||||
persistSession(payload.token);
|
|
||||||
state.profile = payload;
|
|
||||||
ensureDashboardRoute();
|
|
||||||
} catch (error) {
|
|
||||||
state.error = error.message || "Google sign-in failed";
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 || !state.config?.auth?.google?.enabled || !state.config?.auth?.google?.client_id) return;
|
|
||||||
if (!window.google?.accounts?.id) return;
|
|
||||||
googleBox.innerHTML = "";
|
|
||||||
window.google.accounts.id.initialize({
|
|
||||||
client_id: state.config.auth.google.client_id,
|
|
||||||
callback: ({ credential }) => signInWithGoogle(credential),
|
|
||||||
});
|
|
||||||
window.google.accounts.id.renderButton(googleBox, {
|
|
||||||
theme: "outline",
|
|
||||||
size: "large",
|
|
||||||
shape: "pill",
|
|
||||||
text: "signup_with",
|
|
||||||
width: 320,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStatusPills() {
|
function renderStatusPills() {
|
||||||
if (!state.config) return "";
|
if (!state.config) return "";
|
||||||
return `
|
return `
|
||||||
<div class="pill-row">
|
<div class="pill-row">
|
||||||
<span class="pill">API: ${escapeHtml(state.config.public_api_base_url)}</span>
|
<span class="pill">${escapeHtml(copy("status.api"))}: ${escapeHtml(state.config.public_api_base_url)}</span>
|
||||||
<span class="pill">Web: ${escapeHtml(state.config.public_base_url)}</span>
|
<span class="pill">${escapeHtml(copy("status.web"))}: ${escapeHtml(state.config.public_base_url)}</span>
|
||||||
<span class="pill">DevCloud API Image Ready</span>
|
<span class="pill">${escapeHtml(copy("status.devcloud"))}</span>
|
||||||
</div>
|
</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() {
|
function renderSignin() {
|
||||||
const googleConfig = state.config?.auth?.google || {};
|
|
||||||
const googleEnabled = Boolean(googleConfig.enabled && googleConfig.client_id);
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<main class="signin-layout">
|
<main class="signin-layout">
|
||||||
<section class="signin-brand">
|
<section class="signin-brand">
|
||||||
<div class="brand-mark">A</div>
|
<div class="brand-mark">A</div>
|
||||||
<p class="brand-eyebrow">Aurask</p>
|
<p class="brand-eyebrow">${escapeHtml(copy("signin.brand"))}</p>
|
||||||
<h1>Ship AI workflows with private knowledge, safer by default.</h1>
|
<h1>${escapeHtml(copy("signin.heroTitle"))}</h1>
|
||||||
<p class="brand-copy">
|
<p class="brand-copy">${escapeHtml(copy("signin.heroCopy"))}</p>
|
||||||
Sign in to open your personal workspace, manage Langflow workflows, and use AnythingLLM knowledge bases from one portal.
|
|
||||||
</p>
|
|
||||||
${renderStatusPills()}
|
${renderStatusPills()}
|
||||||
</section>
|
</section>
|
||||||
<section class="signin-card">
|
<section class="signin-card">
|
||||||
<div class="signin-header">
|
<div class="signin-header">
|
||||||
<p class="section-kicker">Welcome back</p>
|
<div class="signin-header-top">
|
||||||
<h2>Sign in to Aurask</h2>
|
<p class="section-kicker">${escapeHtml(copy("signin.welcomeBack"))}</p>
|
||||||
<p>Continue with Google to access your workspace or create a new one on first sign-in.</p>
|
${renderLocaleSwitcher()}
|
||||||
|
</div>
|
||||||
|
<h2>${escapeHtml(copy("signin.title"))}</h2>
|
||||||
|
<p>${escapeHtml(copy("signin.subtitle"))}</p>
|
||||||
</div>
|
</div>
|
||||||
${googleEnabled ? '<div id="googleButton" class="google-box"></div>' : '<div class="signin-actions"><button class="auth-btn auth-btn-secondary" type="button" disabled>Continue with Google</button></div>'}
|
${renderSigninGoogleArea()}
|
||||||
${
|
<p class="signin-tip">${escapeHtml(renderSigninHelper())}</p>
|
||||||
googleEnabled
|
${state.errorKey ? `<p class="message error">${renderError()}</p>` : ""}
|
||||||
? '<p class="signin-tip">Google new-user login provisions a dedicated Aurask workspace on first sign-in.</p>'
|
|
||||||
: '<p class="signin-tip">Set `AURASK_GOOGLE_CLIENT_ID` to enable Google one-tap registration.</p>'
|
|
||||||
}
|
|
||||||
${state.error ? `<p class="message error">${escapeHtml(state.error)}</p>` : ""}
|
|
||||||
<div class="signin-footer">
|
<div class="signin-footer">
|
||||||
<span>Need a first workspace?</span>
|
<span>${escapeHtml(copy("signin.needWorkspace"))}</span>
|
||||||
<span>Aurask creates one automatically after your first successful login.</span>
|
<span>${escapeHtml(copy("signin.workspaceAuto"))}</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-locale]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => setLocale(button.dataset.locale));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDashboard() {
|
function renderDashboard() {
|
||||||
@ -229,25 +394,30 @@ function renderDashboard() {
|
|||||||
const config = state.profile?.config || state.config || {};
|
const config = state.profile?.config || state.config || {};
|
||||||
const langflowUrl = withEmbedContext(config.embeds?.langflow_url);
|
const langflowUrl = withEmbedContext(config.embeds?.langflow_url);
|
||||||
const anythingllmUrl = withEmbedContext(config.embeds?.anythingllm_url);
|
const anythingllmUrl = withEmbedContext(config.embeds?.anythingllm_url);
|
||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<main class="dashboard-shell">
|
<main class="dashboard-shell">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="topbar-left">
|
<div class="topbar-left">
|
||||||
<div class="brand-mark brand-mark-small">A</div>
|
<div class="brand-mark brand-mark-small">A</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="section-kicker">Aurask Workspace</p>
|
<p class="section-kicker">${escapeHtml(copy("dashboard.workspaceKicker"))}</p>
|
||||||
<h1>${escapeHtml(tenant.name || "Aurask")}</h1>
|
<h1>${escapeHtml(tenant.name || "Aurask")}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<nav class="tabbar" aria-label="Workspace sections">
|
<nav class="tabbar" aria-label="${escapeHtml(copy("dashboard.workspaceSections"))}">
|
||||||
<button class="tab ${state.activeTab === "workflows" ? "is-active" : ""}" data-tab="workflows" type="button">Workflows</button>
|
<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">Knowledge Base</button>
|
<button class="tab ${state.activeTab === "knowledge" ? "is-active" : ""}" data-tab="knowledge" type="button">${escapeHtml(copy("dashboard.tabs.knowledge"))}</button>
|
||||||
</nav>
|
</nav>
|
||||||
<details class="profile-menu">
|
<details class="profile-menu">
|
||||||
<summary>
|
<summary>
|
||||||
${user.avatar_url ? `<img class="avatar-image" src="${escapeHtml(user.avatar_url)}" alt="${escapeHtml(user.display_name || user.email)}" />` : `<span class="avatar">${escapeHtml(initials(user.display_name || user.email))}</span>`}
|
${
|
||||||
<span class="profile-name">${escapeHtml(user.display_name || user.email || "Profile")}</span>
|
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>
|
</summary>
|
||||||
<div class="profile-panel">
|
<div class="profile-panel">
|
||||||
<div class="profile-block">
|
<div class="profile-block">
|
||||||
@ -255,22 +425,22 @@ function renderDashboard() {
|
|||||||
<span>${escapeHtml(user.email || "")}</span>
|
<span>${escapeHtml(user.email || "")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-block">
|
<div class="profile-block">
|
||||||
<span>Tenant</span>
|
<span>${escapeHtml(copy("dashboard.tenant"))}</span>
|
||||||
<strong>${escapeHtml(tenant.name || "")}</strong>
|
<strong>${escapeHtml(tenant.name || "")}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-block">
|
<div class="profile-block">
|
||||||
<span>Workspace</span>
|
<span>${escapeHtml(copy("dashboard.workspace"))}</span>
|
||||||
<strong>${escapeHtml(workspace.name || "")}</strong>
|
<strong>${escapeHtml(workspace.name || "")}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-block profile-grid">
|
<div class="profile-block profile-grid">
|
||||||
<span>Plan</span>
|
<span>${escapeHtml(copy("dashboard.plan"))}</span>
|
||||||
<strong>${escapeHtml(quota.plan_code || "")}</strong>
|
<strong>${escapeHtml(formatPlanName(quota.plan_code))}</strong>
|
||||||
<span>TBU</span>
|
<span>${escapeHtml(copy("dashboard.tbu"))}</span>
|
||||||
<strong>${escapeHtml(String(quota.available_tbu ?? 0))}</strong>
|
<strong>${escapeHtml(String(quota.available_tbu ?? 0))}</strong>
|
||||||
<span>Knowledge Bases</span>
|
<span>${escapeHtml(copy("dashboard.knowledgeBases"))}</span>
|
||||||
<strong>${escapeHtml(String(quota.knowledge_bases ?? 0))}</strong>
|
<strong>${escapeHtml(String(quota.knowledge_bases ?? 0))}</strong>
|
||||||
</div>
|
</div>
|
||||||
<button id="logoutButton" class="auth-btn auth-btn-secondary" type="button">Sign out</button>
|
<button id="logoutButton" class="auth-btn auth-btn-secondary" type="button">${escapeHtml(copy("dashboard.signOut"))}</button>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@ -278,34 +448,34 @@ function renderDashboard() {
|
|||||||
<section class="dashboard-grid">
|
<section class="dashboard-grid">
|
||||||
<aside class="side-panel">
|
<aside class="side-panel">
|
||||||
<section class="info-card">
|
<section class="info-card">
|
||||||
<p class="section-kicker">Personal Center</p>
|
<p class="section-kicker">${escapeHtml(copy("dashboard.personalCenter"))}</p>
|
||||||
<h2>${escapeHtml(user.display_name || user.email || "Aurask User")}</h2>
|
<h2>${escapeHtml(user.display_name || user.email || copy("dashboard.userFallback"))}</h2>
|
||||||
<p>${escapeHtml(user.email || "")}</p>
|
<p>${escapeHtml(user.email || "")}</p>
|
||||||
<div class="stat-list">
|
<div class="stat-list">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span>Tenant ID</span>
|
<span>${escapeHtml(copy("dashboard.tenantId"))}</span>
|
||||||
<strong>${escapeHtml(tenant.id || "")}</strong>
|
<strong>${escapeHtml(tenant.id || "")}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span>Workspace ID</span>
|
<span>${escapeHtml(copy("dashboard.workspaceId"))}</span>
|
||||||
<strong>${escapeHtml(workspace.id || "")}</strong>
|
<strong>${escapeHtml(workspace.id || "")}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span>API Image</span>
|
<span>${escapeHtml(copy("dashboard.apiImage"))}</span>
|
||||||
<strong>${escapeHtml(config.devcloud?.api_image || "")}</strong>
|
<strong>${escapeHtml(config.devcloud?.api_image || "")}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span>Web Image</span>
|
<span>${escapeHtml(copy("dashboard.webImage"))}</span>
|
||||||
<strong>${escapeHtml(config.devcloud?.web_image || "")}</strong>
|
<strong>${escapeHtml(config.devcloud?.web_image || "")}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="info-card">
|
<section class="info-card">
|
||||||
<p class="section-kicker">Deploy Defaults</p>
|
<p class="section-kicker">${escapeHtml(copy("dashboard.deployDefaults"))}</p>
|
||||||
<ul class="plain-list">
|
<ul class="plain-list">
|
||||||
<li>API Gateway: ${escapeHtml(config.public_api_base_url || "")}</li>
|
<li>${escapeHtml(copy("dashboard.apiGateway"))}: ${escapeHtml(config.public_api_base_url || "")}</li>
|
||||||
<li>Langflow Embed: ${escapeHtml(config.embeds?.langflow_url || "")}</li>
|
<li>${escapeHtml(copy("dashboard.langflowEmbed"))}: ${escapeHtml(config.embeds?.langflow_url || "")}</li>
|
||||||
<li>AnythingLLM Embed: ${escapeHtml(config.embeds?.anythingllm_url || "")}</li>
|
<li>${escapeHtml(copy("dashboard.anythingllmEmbed"))}: ${escapeHtml(config.embeds?.anythingllm_url || "")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
@ -313,26 +483,26 @@ function renderDashboard() {
|
|||||||
<div class="embed-card ${state.activeTab === "workflows" ? "" : "is-hidden"}">
|
<div class="embed-card ${state.activeTab === "workflows" ? "" : "is-hidden"}">
|
||||||
<div class="embed-header">
|
<div class="embed-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-kicker">Workflow Studio</p>
|
<p class="section-kicker">${escapeHtml(copy("dashboard.workflowStudio"))}</p>
|
||||||
<h2>Langflow</h2>
|
<h2>${escapeHtml(copy("dashboard.langflow"))}</h2>
|
||||||
</div>
|
</div>
|
||||||
<a href="${escapeHtml(langflowUrl)}" target="_blank" rel="noreferrer">Open in new tab</a>
|
<a href="${escapeHtml(langflowUrl)}" target="_blank" rel="noreferrer">${escapeHtml(copy("dashboard.openNewTab"))}</a>
|
||||||
</div>
|
</div>
|
||||||
<iframe title="Aurask Langflow" src="${escapeHtml(langflowUrl)}"></iframe>
|
<iframe title="${escapeHtml(copy("dashboard.langflow"))}" src="${escapeHtml(langflowUrl)}"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<div class="embed-card ${state.activeTab === "knowledge" ? "" : "is-hidden"}">
|
<div class="embed-card ${state.activeTab === "knowledge" ? "" : "is-hidden"}">
|
||||||
<div class="embed-header">
|
<div class="embed-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-kicker">Knowledge Base</p>
|
<p class="section-kicker">${escapeHtml(copy("dashboard.knowledgeBaseTitle"))}</p>
|
||||||
<h2>AnythingLLM</h2>
|
<h2>${escapeHtml(copy("dashboard.anythingllm"))}</h2>
|
||||||
</div>
|
</div>
|
||||||
<a href="${escapeHtml(anythingllmUrl)}" target="_blank" rel="noreferrer">Open in new tab</a>
|
<a href="${escapeHtml(anythingllmUrl)}" target="_blank" rel="noreferrer">${escapeHtml(copy("dashboard.openNewTab"))}</a>
|
||||||
</div>
|
</div>
|
||||||
<iframe title="Aurask AnythingLLM" src="${escapeHtml(anythingllmUrl)}"></iframe>
|
<iframe title="${escapeHtml(copy("dashboard.anythingllm"))}" src="${escapeHtml(anythingllmUrl)}"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
${state.error ? `<div class="toast error">${escapeHtml(state.error)}</div>` : ""}
|
${state.errorKey ? `<div class="toast error">${renderError()}</div>` : ""}
|
||||||
</main>
|
</main>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -346,47 +516,149 @@ function renderLoading() {
|
|||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<main class="loading-shell">
|
<main class="loading-shell">
|
||||||
<div class="loader"></div>
|
<div class="loader"></div>
|
||||||
<p>Loading Aurask...</p>
|
<p>${escapeHtml(copy("messages.loading"))}</p>
|
||||||
</main>
|
</main>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
|
applyDocumentLanguage();
|
||||||
|
setDocumentTitle();
|
||||||
|
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
renderLoading();
|
renderLoading();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.profile) {
|
if (state.profile) {
|
||||||
renderDashboard();
|
renderDashboard();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSignin();
|
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);
|
window.addEventListener("popstate", render);
|
||||||
|
|
||||||
if (state.sessionToken) {
|
document.addEventListener("visibilitychange", async () => {
|
||||||
document.addEventListener("visibilitychange", async () => {
|
if (document.visibilityState !== "visible" || !state.profile || !state.sessionToken) return;
|
||||||
if (document.visibilityState !== "visible" || !state.profile) return;
|
|
||||||
try {
|
try {
|
||||||
state.profile = await request("/auth/session");
|
state.profile = await request("/auth/session");
|
||||||
render();
|
render();
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
persistSession("");
|
persistSession("");
|
||||||
state.profile = null;
|
state.profile = null;
|
||||||
state.error = "Your Aurask session has expired. Please sign in again.";
|
setError("messages.sessionExpired");
|
||||||
ensureSigninRoute();
|
ensureSigninRoute();
|
||||||
render();
|
render();
|
||||||
mountGoogleButton();
|
mountGoogleButton();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const googleScript = document.createElement("script");
|
const googleScript = document.createElement("script");
|
||||||
googleScript.src = "https://accounts.google.com/gsi/client";
|
googleScript.src = "https://accounts.google.com/gsi/client";
|
||||||
googleScript.async = true;
|
googleScript.async = true;
|
||||||
googleScript.defer = true;
|
googleScript.defer = true;
|
||||||
googleScript.onload = mountGoogleButton;
|
googleScript.onload = () => {
|
||||||
|
state.googleSdkReady = Boolean(window.google?.accounts?.id);
|
||||||
|
if (!state.profile && !state.loading) {
|
||||||
|
render();
|
||||||
|
mountGoogleButton();
|
||||||
|
}
|
||||||
|
};
|
||||||
document.head.appendChild(googleScript);
|
document.head.appendChild(googleScript);
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@ -139,6 +139,13 @@ button {
|
|||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signin-header-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.signin-header h2 {
|
.signin-header h2 {
|
||||||
margin: 8px 0 8px;
|
margin: 8px 0 8px;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
@ -159,6 +166,40 @@ button {
|
|||||||
margin-top: 26px;
|
margin-top: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.google-slot {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-switcher {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #dbe2f0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-btn {
|
||||||
|
min-width: 56px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
color: #64748b;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-btn.is-active {
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, #2563eb, #0ea5e9);
|
||||||
|
box-shadow: 0 10px 22px rgba(37, 99, 235, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.auth-btn {
|
.auth-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -196,11 +237,19 @@ button {
|
|||||||
border: 1px solid #dbe7ff;
|
border: 1px solid #dbe7ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-btn-fallback {
|
||||||
|
color: #64748b;
|
||||||
|
border-color: #d7dee9;
|
||||||
|
background: linear-gradient(180deg, #f3f6fb, #e9eef6);
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.google-box {
|
.google-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
margin-top: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ly-form label {
|
.ly-form label {
|
||||||
@ -567,6 +616,11 @@ iframe {
|
|||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signin-header-top {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.tabbar,
|
.tabbar,
|
||||||
.profile-menu summary,
|
.profile-menu summary,
|
||||||
.profile-panel {
|
.profile-panel {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user