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:
+30
-3
@@ -36,6 +36,10 @@ import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronSe
|
|||||||
import { ElectronLoginForm } from "@/auth/ElectronLoginForm";
|
import { ElectronLoginForm } from "@/auth/ElectronLoginForm";
|
||||||
import { Checkbox } from "@/components/checkbox";
|
import { Checkbox } from "@/components/checkbox";
|
||||||
import i18n from "@/i18n/i18n";
|
import i18n from "@/i18n/i18n";
|
||||||
|
import {
|
||||||
|
removeSilentSigninFromSearch,
|
||||||
|
shouldTriggerSilentSignin,
|
||||||
|
} from "./silent-signin";
|
||||||
|
|
||||||
const LANGUAGES = [
|
const LANGUAGES = [
|
||||||
{ code: "en", label: "English" },
|
{ code: "en", label: "English" },
|
||||||
@@ -234,6 +238,8 @@ export function Auth({ onLogin }: AuthProps) {
|
|||||||
const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true);
|
const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true);
|
||||||
const [passwordResetAllowed, setPasswordResetAllowed] = useState(true);
|
const [passwordResetAllowed, setPasswordResetAllowed] = useState(true);
|
||||||
const [oidcConfigured, setOidcConfigured] = useState(false);
|
const [oidcConfigured, setOidcConfigured] = useState(false);
|
||||||
|
const [oidcConfigLoaded, setOidcConfigLoaded] = useState(false);
|
||||||
|
const silentSigninHandledRef = useRef(false);
|
||||||
const [firstUser, setFirstUser] = useState(false);
|
const [firstUser, setFirstUser] = useState(false);
|
||||||
const [dbConnectionFailed, setDbConnectionFailed] = useState(false);
|
const [dbConnectionFailed, setDbConnectionFailed] = useState(false);
|
||||||
const [dbHealthChecking, setDbHealthChecking] = useState(true);
|
const [dbHealthChecking, setDbHealthChecking] = useState(true);
|
||||||
@@ -262,7 +268,8 @@ export function Auth({ onLogin }: AuthProps) {
|
|||||||
.catch(() => setPasswordResetAllowed(false));
|
.catch(() => setPasswordResetAllowed(false));
|
||||||
getOIDCConfig()
|
getOIDCConfig()
|
||||||
.then((res) => setOidcConfigured(!!res))
|
.then((res) => setOidcConfigured(!!res))
|
||||||
.catch(() => setOidcConfigured(false));
|
.catch(() => setOidcConfigured(false))
|
||||||
|
.finally(() => setOidcConfigLoaded(true));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -643,7 +650,7 @@ export function Auth({ onLogin }: AuthProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOIDCLogin() {
|
const handleOIDCLogin = useCallback(async () => {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
try {
|
try {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
@@ -679,7 +686,27 @@ export function Auth({ onLogin }: AuthProps) {
|
|||||||
);
|
);
|
||||||
setOidcLoading(false);
|
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
|
// Electron server config / webview auth success screens
|
||||||
if (isElectron() && !isInElectronWebView()) {
|
if (isElectron() && !isInElectronWebView()) {
|
||||||
|
|||||||
@@ -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 { Button } from "@/components/button.tsx";
|
||||||
import { Input } from "@/components/input.tsx";
|
import { Input } from "@/components/input.tsx";
|
||||||
import { PasswordInput } from "@/components/password-input.tsx";
|
import { PasswordInput } from "@/components/password-input.tsx";
|
||||||
@@ -31,6 +31,10 @@ import {
|
|||||||
} from "@/main-axios";
|
} from "@/main-axios";
|
||||||
import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig.tsx";
|
import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig.tsx";
|
||||||
import { ElectronLoginForm } from "@/auth/ElectronLoginForm.tsx";
|
import { ElectronLoginForm } from "@/auth/ElectronLoginForm.tsx";
|
||||||
|
import {
|
||||||
|
removeSilentSigninFromSearch,
|
||||||
|
shouldTriggerSilentSignin,
|
||||||
|
} from "./silent-signin";
|
||||||
|
|
||||||
function isMissingServerConfigError(error: unknown): boolean {
|
function isMissingServerConfigError(error: unknown): boolean {
|
||||||
if (!(error instanceof Error)) {
|
if (!(error instanceof Error)) {
|
||||||
@@ -123,6 +127,8 @@ export function Auth({
|
|||||||
const [registrationAllowed, setRegistrationAllowed] = useState(true);
|
const [registrationAllowed, setRegistrationAllowed] = useState(true);
|
||||||
const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true);
|
const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true);
|
||||||
const [oidcConfigured, setOidcConfigured] = useState(false);
|
const [oidcConfigured, setOidcConfigured] = useState(false);
|
||||||
|
const [oidcConfigLoaded, setOidcConfigLoaded] = useState(false);
|
||||||
|
const silentSigninHandledRef = useRef(false);
|
||||||
|
|
||||||
const [resetStep, setResetStep] = useState<
|
const [resetStep, setResetStep] = useState<
|
||||||
"initiate" | "verify" | "newPassword"
|
"initiate" | "verify" | "newPassword"
|
||||||
@@ -248,6 +254,9 @@ export function Auth({
|
|||||||
} else {
|
} else {
|
||||||
setOidcConfigured(false);
|
setOidcConfigured(false);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setOidcConfigLoaded(true);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -625,7 +634,7 @@ export function Auth({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOIDCLogin() {
|
const handleOIDCLogin = useCallback(async () => {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
try {
|
try {
|
||||||
const authResponse = await getOIDCAuthorizeUrl(rememberMe);
|
const authResponse = await getOIDCAuthorizeUrl(rememberMe);
|
||||||
@@ -648,7 +657,27 @@ export function Auth({
|
|||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
setOidcLoading(false);
|
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(() => {
|
useEffect(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|||||||
@@ -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",
|
"failedCompleteReset": "Failed to complete password reset",
|
||||||
"invalidTotpCode": "Invalid TOTP code",
|
"invalidTotpCode": "Invalid TOTP code",
|
||||||
"failedOidcLogin": "Failed to start OIDC login",
|
"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",
|
"failedUserInfo": "Failed to get user info after login",
|
||||||
"oidcAuthFailed": "OIDC authentication failed",
|
"oidcAuthFailed": "OIDC authentication failed",
|
||||||
"invalidAuthUrl": "Invalid authorization URL received from backend",
|
"invalidAuthUrl": "Invalid authorization URL received from backend",
|
||||||
|
|||||||
@@ -2336,6 +2336,7 @@
|
|||||||
"failedCompleteReset": "密码重置失败",
|
"failedCompleteReset": "密码重置失败",
|
||||||
"invalidTotpCode": "无效的 TOTP 代码",
|
"invalidTotpCode": "无效的 TOTP 代码",
|
||||||
"failedOidcLogin": "OIDC 登录启动失败",
|
"failedOidcLogin": "OIDC 登录启动失败",
|
||||||
|
"silentSigninOidcUnavailable": "已请求静默登录,但当前未启用 OIDC 登录。",
|
||||||
"failedUserInfo": "登录后无法获取用户信息",
|
"failedUserInfo": "登录后无法获取用户信息",
|
||||||
"oidcAuthFailed": "OIDC 身份验证失败",
|
"oidcAuthFailed": "OIDC 身份验证失败",
|
||||||
"noTokenReceived": "登录未收到令牌",
|
"noTokenReceived": "登录未收到令牌",
|
||||||
|
|||||||
Reference in New Issue
Block a user