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 && (
+
)}
- ) : (
-