mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
feat(auth): add silent OIDC sign-in (#799)
This commit is contained in:
+29
-2
@@ -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,8 +686,28 @@ 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()) {
|
||||
if (showServerConfig === null)
|
||||
|
||||
@@ -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,8 +657,28 @@ 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);
|
||||
const success = urlParams.get("success");
|
||||
|
||||
@@ -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}` : "";
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -2336,6 +2336,7 @@
|
||||
"failedCompleteReset": "密码重置失败",
|
||||
"invalidTotpCode": "无效的 TOTP 代码",
|
||||
"failedOidcLogin": "OIDC 登录启动失败",
|
||||
"silentSigninOidcUnavailable": "已请求静默登录,但当前未启用 OIDC 登录。",
|
||||
"failedUserInfo": "登录后无法获取用户信息",
|
||||
"oidcAuthFailed": "OIDC 身份验证失败",
|
||||
"noTokenReceived": "登录未收到令牌",
|
||||
|
||||
Reference in New Issue
Block a user