diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e160f094..732fe4ee 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1252,6 +1252,7 @@ "enterCode": "Enter verification code", "backupCode": "Or use backup code", "verifyCode": "Verify Code", + "redirectingToApp": "Redirecting to app...", "enableTwoFactor": "Enable Two-Factor Authentication", "disableTwoFactor": "Disable Two-Factor Authentication", "scanQRCode": "Scan this QR code with your authenticator app", diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index 5ad4c465..1bedd2c1 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { LeftSidebar } from "@/ui/desktop/navigation/LeftSidebar.tsx"; import { Dashboard } from "@/ui/desktop/apps/dashboard/Dashboard.tsx"; import { AppView } from "@/ui/desktop/navigation/AppView.tsx"; @@ -68,15 +68,18 @@ function AppContent() { const handleSelectView = () => {}; - const handleAuthSuccess = (authData: { - isAdmin: boolean; - username: string | null; - userId: string | null; - }) => { - setIsAuthenticated(true); - setIsAdmin(authData.isAdmin); - setUsername(authData.username); - }; + const handleAuthSuccess = useCallback( + (authData: { + isAdmin: boolean; + username: string | null; + userId: string | null; + }) => { + setIsAuthenticated(true); + setIsAdmin(authData.isAdmin); + setUsername(authData.username); + }, + [], + ); const currentTabData = tabs.find((tab) => tab.id === currentTab); const showTerminalView = diff --git a/src/ui/desktop/authentication/Auth.tsx b/src/ui/desktop/authentication/Auth.tsx index 0de49c8d..eaa82600 100644 --- a/src/ui/desktop/authentication/Auth.tsx +++ b/src/ui/desktop/authentication/Auth.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils.ts"; import { Button } from "@/components/ui/button.tsx"; import { Input } from "@/components/ui/input.tsx"; @@ -105,6 +105,33 @@ export function Auth({ const [totpLoading, setTotpLoading] = useState(false); const [webviewAuthSuccess, setWebviewAuthSuccess] = useState(false); + const handleElectronAuthSuccess = useCallback(async () => { + try { + const meRes = await getUserInfo(); + setInternalLoggedIn(true); + setLoggedIn(true); + setIsAdmin(!!meRes.is_admin); + setUsername(meRes.username || null); + setUserId(meRes.userId || null); + onAuthSuccess({ + isAdmin: !!meRes.is_admin, + username: meRes.username || null, + userId: meRes.userId || null, + }); + toast.success(t("messages.loginSuccess")); + } catch (err) { + toast.error(t("errors.failedUserInfo")); + } + }, [ + onAuthSuccess, + setLoggedIn, + setIsAdmin, + setUsername, + setUserId, + t, + setInternalLoggedIn, + ]); + useEffect(() => { setInternalLoggedIn(loggedIn); }, [loggedIn]); @@ -688,24 +715,7 @@ export function Auth({
{ - try { - const meRes = await getUserInfo(); - setInternalLoggedIn(true); - setLoggedIn(true); - setIsAdmin(!!meRes.is_admin); - setUsername(meRes.username || null); - setUserId(meRes.userId || null); - onAuthSuccess({ - isAdmin: !!meRes.is_admin, - username: meRes.username || null, - userId: meRes.userId || null, - }); - toast.success(t("messages.loginSuccess")); - } catch (err) { - toast.error(t("errors.failedUserInfo")); - } - }} + onAuthSuccess={handleElectronAuthSuccess} onChangeServer={() => { setShowServerConfig(true); }} diff --git a/src/ui/desktop/authentication/ElectronLoginForm.tsx b/src/ui/desktop/authentication/ElectronLoginForm.tsx index 131c84ce..905c08cc 100644 --- a/src/ui/desktop/authentication/ElectronLoginForm.tsx +++ b/src/ui/desktop/authentication/ElectronLoginForm.tsx @@ -57,8 +57,7 @@ export function ElectronLoginForm({ if ( data.type === "AUTH_SUCCESS" && data.token && - !hasAuthenticatedRef.current && - !isAuthenticating + !hasAuthenticatedRef.current ) { hasAuthenticatedRef.current = true; setIsAuthenticating(true); @@ -110,7 +109,7 @@ export function ElectronLoginForm({ return () => { window.removeEventListener("message", handleMessage); }; - }, [serverUrl, isAuthenticating, onAuthSuccess, t]); + }, [serverUrl, onAuthSuccess, t]); useEffect(() => { const checkWebviewUrl = () => { diff --git a/src/ui/mobile/authentication/Auth.tsx b/src/ui/mobile/authentication/Auth.tsx index 44e97586..95a2622c 100644 --- a/src/ui/mobile/authentication/Auth.tsx +++ b/src/ui/mobile/authentication/Auth.tsx @@ -93,7 +93,7 @@ export function Auth({ const [signupConfirmPassword, setSignupConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); const [oidcLoading, setOidcLoading] = useState(false); - const [, setError] = useState(null); + const [error, setError] = useState(null); const [internalLoggedIn, setInternalLoggedIn] = useState(false); const [firstUser, setFirstUser] = useState(false); const [firstUserToastShown, setFirstUserToastShown] = useState(false); @@ -115,6 +115,7 @@ export function Auth({ const [totpCode, setTotpCode] = useState(""); const [totpTempToken, setTotpTempToken] = useState(""); const [totpLoading, setTotpLoading] = useState(false); + const [mobileAuthSuccess, setMobileAuthSuccess] = useState(false); useEffect(() => { setInternalLoggedIn(loggedIn); @@ -238,7 +239,6 @@ export function Auth({ const [meRes] = await Promise.all([getUserInfo()]); - setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); @@ -252,6 +252,12 @@ export function Auth({ postJWTToWebView(); + if (isReactNativeWebView()) { + setMobileAuthSuccess(true); + setLoading(false); + return; + } + setInternalLoggedIn(true); if (tab === "signup") { setSignupConfirmPassword(""); @@ -271,7 +277,7 @@ export function Auth({ error?.response?.data?.error || error?.message || t("errors.unknownError"); - toast.error(errorMessage); + setError(errorMessage); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); @@ -299,11 +305,11 @@ export function Auth({ message?: string; response?: { data?: { error?: string } }; }; - toast.error( + const errorMessage = error?.response?.data?.error || - error?.message || - t("errors.failedPasswordReset"), - ); + error?.message || + t("errors.failedPasswordReset"); + setError(errorMessage); } finally { setResetLoading(false); } @@ -319,7 +325,9 @@ export function Auth({ toast.success(t("messages.codeVerified")); } catch (err: unknown) { const error = err as { response?: { data?: { error?: string } } }; - toast.error(error?.response?.data?.error || t("errors.failedVerifyCode")); + const errorMessage = + error?.response?.data?.error || t("errors.failedVerifyCode"); + setError(errorMessage); } finally { setResetLoading(false); } @@ -358,9 +366,9 @@ export function Auth({ resetPasswordState(); } catch (err: unknown) { const error = err as { response?: { data?: { error?: string } } }; - toast.error( - error?.response?.data?.error || t("errors.failedCompleteReset"), - ); + const errorMessage = + error?.response?.data?.error || t("errors.failedCompleteReset"); + setError(errorMessage); } finally { setResetLoading(false); } @@ -403,22 +411,24 @@ export function Auth({ localStorage.setItem("jwt", res.token); } - setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!res.is_admin); setUsername(res.username || null); setUserId(res.userId || null); setDbError(null); - setTimeout(() => { - onAuthSuccess({ - isAdmin: !!res.is_admin, - username: res.username || null, - userId: res.userId || null, - }); + onAuthSuccess({ + isAdmin: !!res.is_admin, + username: res.username || null, + userId: res.userId || null, + }); + postJWTToWebView(); - postJWTToWebView(); - }, 100); + if (isReactNativeWebView()) { + setMobileAuthSuccess(true); + setTotpLoading(false); + return; + } setInternalLoggedIn(true); setTotpRequired(false); @@ -443,7 +453,7 @@ export function Auth({ setTab("login"); toast.error(t("errors.sessionExpired")); } else { - toast.error(errorMessage); + setError(errorMessage); } } finally { setTotpLoading(false); @@ -471,7 +481,7 @@ export function Auth({ error?.response?.data?.error || error?.message || t("errors.failedOidcLogin"); - toast.error(errorMessage); + setError(errorMessage); setOidcLoading(false); } } @@ -482,7 +492,8 @@ export function Auth({ const error = urlParams.get("error"); if (error) { - toast.error(`${t("errors.oidcAuthFailed")}: ${error}`); + const errorMessage = `${t("errors.oidcAuthFailed")}: ${error}`; + setError(errorMessage); setOidcLoading(false); window.history.replaceState({}, document.title, window.location.pathname); return; @@ -494,7 +505,6 @@ export function Auth({ getUserInfo() .then((meRes) => { - setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); @@ -508,6 +518,17 @@ export function Auth({ postJWTToWebView(); + if (isReactNativeWebView()) { + setMobileAuthSuccess(true); + setOidcLoading(false); + window.history.replaceState( + {}, + document.title, + window.location.pathname, + ); + return; + } + setInternalLoggedIn(true); window.history.replaceState( {}, @@ -516,7 +537,7 @@ export function Auth({ ); }) .catch(() => { - toast.error(t("errors.failedUserInfo")); + setError(t("errors.failedUserInfo")); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); @@ -562,20 +583,53 @@ export function Auth({ style={{ maxHeight: "calc(100vh - 1rem)" }} {...props} > - {isReactNativeWebView() && ( + {isReactNativeWebView() && !mobileAuthSuccess && ( {t("auth.mobileApp")} {t("auth.loggingInToMobileApp")} )} - {dbError && ( + {isReactNativeWebView() && mobileAuthSuccess && ( +
+
+ + + +
+
+

+ {t("messages.loginSuccess")} +

+

+ {t("auth.redirectingToApp")} +

+
+
+ )} + {!mobileAuthSuccess && error && ( + + {t("common.error", "Error")} + {error} + + )} + {!mobileAuthSuccess && dbError && ( Error {dbError} )} - {totpRequired && ( + {!mobileAuthSuccess && totpRequired && (

@@ -628,7 +682,7 @@ export function Auth({

)} - {internalLoggedIn && !authLoading && ( + {!mobileAuthSuccess && internalLoggedIn && !authLoading && (

@@ -652,397 +706,410 @@ export function Auth({

)} - {!internalLoggedIn && !authLoading && !totpRequired && ( - <> - {(() => { - const hasLogin = passwordLoginAllowed && !firstUser; - const hasSignup = - (passwordLoginAllowed || firstUser) && registrationAllowed; - const hasOIDC = oidcConfigured; - const hasAnyAuth = hasLogin || hasSignup || hasOIDC; + {!mobileAuthSuccess && + !internalLoggedIn && + !authLoading && + !totpRequired && ( + <> + {(() => { + const hasLogin = passwordLoginAllowed && !firstUser; + const hasSignup = + (passwordLoginAllowed || firstUser) && registrationAllowed; + const hasOIDC = oidcConfigured; + const hasAnyAuth = hasLogin || hasSignup || hasOIDC; + + if (!hasAnyAuth) { + return ( +
+

+ {t("auth.authenticationDisabled")} +

+

+ {t("auth.authenticationDisabledDesc")} +

+
+ ); + } - if (!hasAnyAuth) { return ( -
-

- {t("auth.authenticationDisabled")} -

-

- {t("auth.authenticationDisabledDesc")} -

-
- ); - } - - return ( - <> -
- {passwordLoginAllowed && ( - - )} - {(passwordLoginAllowed || firstUser) && - registrationAllowed && ( + <> +
+ {passwordLoginAllowed && ( )} - {oidcConfigured && ( - - )} -
-
-

- {tab === "login" - ? t("auth.loginTitle") - : tab === "signup" - ? t("auth.registerTitle") - : tab === "external" - ? t("auth.loginWithExternal") - : t("auth.forgotPassword")} -

-
- - {tab === "external" || tab === "reset" ? ( -
- {tab === "external" && ( - <> -
-

{t("auth.loginWithExternalDesc")}

-
- - - )} - {tab === "reset" && ( - <> - {resetStep === "initiate" && ( - <> - - {t("common.warning")} - - {t("auth.dataLossWarning")} - - -
-

{t("auth.resetCodeDesc")}

-
-
-
- - - setLocalUsername(e.target.value) - } - disabled={resetLoading} - /> -
- -
- + {t("common.register")} + + )} + {oidcConfigured && ( + - -
- - )} - - {resetStep === "newPassword" && !resetSuccess && ( - <> -
-

- {t("auth.enterNewPassword")}{" "} - {localUsername} -

-
-
-
- - - setNewPassword(e.target.value) - } - disabled={resetLoading} - autoComplete="new-password" - /> -
-
- - - setConfirmPassword(e.target.value) - } - disabled={resetLoading} - autoComplete="new-password" - /> -
- - -
- - )} - + onClick={() => { + setTab("external"); + if (tab === "reset") resetPasswordState(); + if (tab === "login" || tab === "signup") + clearFormFields(); + }} + aria-selected={tab === "external"} + disabled={oidcLoading} + > + {t("auth.external")} + )}
- ) : ( -
-
- - setLocalUsername(e.target.value)} - disabled={loading || internalLoggedIn} - /> +
+

+ {tab === "login" + ? t("auth.loginTitle") + : tab === "signup" + ? t("auth.registerTitle") + : tab === "external" + ? t("auth.loginWithExternal") + : t("auth.forgotPassword")} +

+
+ + {tab === "external" || tab === "reset" ? ( +
+ {tab === "external" && ( + <> +
+

{t("auth.loginWithExternalDesc")}

+
+ + + )} + {tab === "reset" && ( + <> + {resetStep === "initiate" && ( + <> + + {t("common.warning")} + + {t("auth.dataLossWarning")} + + +
+

{t("auth.resetCodeDesc")}

+
+
+
+ + + setLocalUsername(e.target.value) + } + disabled={resetLoading} + /> +
+ +
+ + )} + + {resetStep === "verify" && ( + <> +
+

+ {t("auth.enterResetCode")}{" "} + {localUsername} +

+
+
+
+ + + setResetCode( + e.target.value.replace(/\D/g, ""), + ) + } + disabled={resetLoading} + placeholder="000000" + /> +
+ + +
+ + )} + + {resetStep === "newPassword" && !resetSuccess && ( + <> +
+

+ {t("auth.enterNewPassword")}{" "} + {localUsername} +

+
+
+
+ + + setNewPassword(e.target.value) + } + disabled={resetLoading} + autoComplete="new-password" + /> +
+
+ + + setConfirmPassword(e.target.value) + } + disabled={resetLoading} + autoComplete="new-password" + /> +
+ + +
+ + )} + + )}
-
- - setPassword(e.target.value)} - disabled={loading || internalLoggedIn} - /> -
- {tab === "signup" && ( + ) : ( +
- - {t("common.username")} + - setSignupConfirmPassword(e.target.value) - } + value={localUsername} + onChange={(e) => setLocalUsername(e.target.value)} disabled={loading || internalLoggedIn} />
- )} - - {tab === "login" && ( +
+ + setPassword(e.target.value)} + disabled={loading || internalLoggedIn} + /> +
+ {tab === "signup" && ( +
+ + + setSignupConfirmPassword(e.target.value) + } + disabled={loading || internalLoggedIn} + /> +
+ )} - )} - - )} + {tab === "login" && ( + + )} + + )} -
-
-
- +
+
+
+ +
+
-
-
-
- -
- - ); - })()} - - )} +
+ +
+ + ); + })()} + + )}
); }