diff --git a/src/ui/auth/Auth.tsx b/src/ui/auth/Auth.tsx index 4ff8776f..7bbb7fa5 100644 --- a/src/ui/auth/Auth.tsx +++ b/src/ui/auth/Auth.tsx @@ -36,6 +36,10 @@ import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronSe import { ElectronLoginForm } from "@/auth/ElectronLoginForm"; import { Checkbox } from "@/components/checkbox"; import i18n from "@/i18n/i18n"; +import { + removeSilentSigninFromSearch, + shouldTriggerSilentSignin, +} from "./silent-signin"; const LANGUAGES = [ { code: "en", label: "English" }, @@ -234,6 +238,8 @@ export function Auth({ onLogin }: AuthProps) { const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true); const [passwordResetAllowed, setPasswordResetAllowed] = useState(true); const [oidcConfigured, setOidcConfigured] = useState(false); + const [oidcConfigLoaded, setOidcConfigLoaded] = useState(false); + const silentSigninHandledRef = useRef(false); const [firstUser, setFirstUser] = useState(false); const [dbConnectionFailed, setDbConnectionFailed] = useState(false); const [dbHealthChecking, setDbHealthChecking] = useState(true); @@ -262,7 +268,8 @@ export function Auth({ onLogin }: AuthProps) { .catch(() => setPasswordResetAllowed(false)); getOIDCConfig() .then((res) => setOidcConfigured(!!res)) - .catch(() => setOidcConfigured(false)); + .catch(() => setOidcConfigured(false)) + .finally(() => setOidcConfigLoaded(true)); }, []); useEffect(() => { @@ -643,7 +650,7 @@ export function Auth({ onLogin }: AuthProps) { } } - async function handleOIDCLogin() { + const handleOIDCLogin = useCallback(async () => { setOidcLoading(true); try { if (isElectron()) { @@ -679,7 +686,27 @@ export function Auth({ onLogin }: AuthProps) { ); setOidcLoading(false); } - } + }, [rememberMe, t]); + + useEffect(() => { + if (!oidcConfigLoaded || silentSigninHandledRef.current) return; + if (!shouldTriggerSilentSignin(window.location.search)) return; + + const nextSearch = removeSilentSigninFromSearch(window.location.search); + window.history.replaceState( + {}, + document.title, + `${window.location.pathname}${nextSearch}${window.location.hash}`, + ); + + silentSigninHandledRef.current = true; + if (oidcConfigured && !isElectron()) { + handleOIDCLogin(); + return; + } + + toast.info(t("errors.silentSigninOidcUnavailable")); + }, [handleOIDCLogin, oidcConfigLoaded, oidcConfigured, t]); // Electron server config / webview auth success screens if (isElectron() && !isInElectronWebView()) { diff --git a/src/ui/auth/LoginPage.tsx b/src/ui/auth/LoginPage.tsx index 4818e3c5..0569958d 100644 --- a/src/ui/auth/LoginPage.tsx +++ b/src/ui/auth/LoginPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { Button } from "@/components/button.tsx"; import { Input } from "@/components/input.tsx"; import { PasswordInput } from "@/components/password-input.tsx"; @@ -31,6 +31,10 @@ import { } from "@/main-axios"; import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig.tsx"; import { ElectronLoginForm } from "@/auth/ElectronLoginForm.tsx"; +import { + removeSilentSigninFromSearch, + shouldTriggerSilentSignin, +} from "./silent-signin"; function isMissingServerConfigError(error: unknown): boolean { if (!(error instanceof Error)) { @@ -123,6 +127,8 @@ export function Auth({ const [registrationAllowed, setRegistrationAllowed] = useState(true); const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true); const [oidcConfigured, setOidcConfigured] = useState(false); + const [oidcConfigLoaded, setOidcConfigLoaded] = useState(false); + const silentSigninHandledRef = useRef(false); const [resetStep, setResetStep] = useState< "initiate" | "verify" | "newPassword" @@ -248,6 +254,9 @@ export function Auth({ } else { setOidcConfigured(false); } + }) + .finally(() => { + setOidcConfigLoaded(true); }); }, []); @@ -625,7 +634,7 @@ export function Auth({ } } - async function handleOIDCLogin() { + const handleOIDCLogin = useCallback(async () => { setOidcLoading(true); try { const authResponse = await getOIDCAuthorizeUrl(rememberMe); @@ -648,7 +657,27 @@ export function Auth({ toast.error(errorMessage); setOidcLoading(false); } - } + }, [rememberMe, t]); + + useEffect(() => { + if (!oidcConfigLoaded || silentSigninHandledRef.current) return; + if (!shouldTriggerSilentSignin(window.location.search)) return; + + const nextSearch = removeSilentSigninFromSearch(window.location.search); + window.history.replaceState( + {}, + document.title, + `${window.location.pathname}${nextSearch}${window.location.hash}`, + ); + + silentSigninHandledRef.current = true; + if (oidcConfigured && !isElectron()) { + handleOIDCLogin(); + return; + } + + toast.info(t("errors.silentSigninOidcUnavailable")); + }, [handleOIDCLogin, oidcConfigLoaded, oidcConfigured, t]); useEffect(() => { const urlParams = new URLSearchParams(window.location.search); diff --git a/src/ui/auth/silent-signin.ts b/src/ui/auth/silent-signin.ts new file mode 100644 index 00000000..60a1e754 --- /dev/null +++ b/src/ui/auth/silent-signin.ts @@ -0,0 +1,18 @@ +export function shouldTriggerSilentSignin(search: string) { + const params = new URLSearchParams(search); + for (const [key, rawValue] of params) { + if (key.toLowerCase() !== "silentsignin") continue; + const value = rawValue; + return value === "" || value === "true" || value === "1"; + } + return false; +} + +export function removeSilentSigninFromSearch(search: string) { + const params = new URLSearchParams(search); + for (const key of Array.from(params.keys())) { + if (key.toLowerCase() === "silentsignin") params.delete(key); + } + const nextSearch = params.toString(); + return nextSearch ? `?${nextSearch}` : ""; +} diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 8be051d5..62fe727a 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -1226,6 +1226,7 @@ "failedCompleteReset": "Failed to complete password reset", "invalidTotpCode": "Invalid TOTP code", "failedOidcLogin": "Failed to start OIDC login", + "silentSigninOidcUnavailable": "Silent sign-in was requested, but OIDC login is not available.", "failedUserInfo": "Failed to get user info after login", "oidcAuthFailed": "OIDC authentication failed", "invalidAuthUrl": "Invalid authorization URL received from backend", diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json index baa5e1bd..34fddb14 100644 --- a/src/ui/locales/translated/zh_CN.json +++ b/src/ui/locales/translated/zh_CN.json @@ -2336,6 +2336,7 @@ "failedCompleteReset": "密码重置失败", "invalidTotpCode": "无效的 TOTP 代码", "failedOidcLogin": "OIDC 登录启动失败", + "silentSigninOidcUnavailable": "已请求静默登录,但当前未启用 OIDC 登录。", "failedUserInfo": "登录后无法获取用户信息", "oidcAuthFailed": "OIDC 身份验证失败", "noTokenReceived": "登录未收到令牌",