import React, { useState, useEffect } from "react"; import { cn } from "@/lib/utils.ts"; import { Button } from "@/components/ui/button.tsx"; import { Input } from "@/components/ui/input.tsx"; import { PasswordInput } from "@/components/ui/password-input.tsx"; import { Label } from "@/components/ui/label.tsx"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; import { useTranslation } from "react-i18next"; import { LanguageSwitcher } from "@/ui/desktop/user/LanguageSwitcher.tsx"; import { toast } from "sonner"; import { Monitor } from "lucide-react"; import { registerUser, loginUser, getUserInfo, getRegistrationAllowed, getPasswordLoginAllowed, getOIDCConfig, getSetupRequired, initiatePasswordReset, verifyPasswordResetCode, completePasswordReset, getOIDCAuthorizeUrl, verifyTOTPLogin, getServerConfig, isElectron, logoutUser, } from "../../main-axios.ts"; import { ElectronServerConfig as ServerConfigComponent } from "@/ui/desktop/authentication/ElectronServerConfig.tsx"; import { ElectronLoginForm } from "@/ui/desktop/authentication/ElectronLoginForm.tsx"; interface AuthProps extends React.ComponentProps<"div"> { setLoggedIn: (loggedIn: boolean) => void; setIsAdmin: (isAdmin: boolean) => void; setUsername: (username: string | null) => void; setUserId: (userId: string | null) => void; loggedIn: boolean; authLoading: boolean; setDbError: (error: string | null) => void; dbError?: string | null; onAuthSuccess: (authData: { isAdmin: boolean; username: string | null; userId: string | null; }) => void; } export function Auth({ className, setLoggedIn, setIsAdmin, setUsername, setUserId, loggedIn, authLoading, setDbError, dbError, onAuthSuccess, ...props }: AuthProps) { const { t } = useTranslation(); const isInElectronWebView = () => { if ((window as any).IS_ELECTRON_WEBVIEW) { return true; } try { if (window.self !== window.top) { return true; } } catch (e) { return false; } return false; }; const [tab, setTab] = useState<"login" | "signup" | "external" | "reset">( "login", ); const [localUsername, setLocalUsername] = useState(""); const [password, setPassword] = useState(""); const [signupConfirmPassword, setSignupConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); const [oidcLoading, setOidcLoading] = useState(false); const [internalLoggedIn, setInternalLoggedIn] = useState(false); const [firstUser, setFirstUser] = useState(false); const [firstUserToastShown, setFirstUserToastShown] = useState(false); const [registrationAllowed, setRegistrationAllowed] = useState(true); const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true); const [oidcConfigured, setOidcConfigured] = useState(false); const [resetStep, setResetStep] = useState< "initiate" | "verify" | "newPassword" >("initiate"); const [resetCode, setResetCode] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [tempToken, setTempToken] = useState(""); const [resetLoading, setResetLoading] = useState(false); const [resetSuccess, setResetSuccess] = useState(false); const [totpRequired, setTotpRequired] = useState(false); const [totpCode, setTotpCode] = useState(""); const [totpTempToken, setTotpTempToken] = useState(""); const [totpLoading, setTotpLoading] = useState(false); const [webviewAuthSuccess, setWebviewAuthSuccess] = useState(false); useEffect(() => { setInternalLoggedIn(loggedIn); }, [loggedIn]); useEffect(() => { getRegistrationAllowed().then((res) => { setRegistrationAllowed(res.allowed); }); }, []); useEffect(() => { getPasswordLoginAllowed() .then((res) => { setPasswordLoginAllowed(res.allowed); }) .catch((err) => { if (err.code !== "NO_SERVER_CONFIGURED") { console.error("Failed to fetch password login status:", err); } }); }, []); useEffect(() => { getOIDCConfig() .then((response) => { if (response) { setOidcConfigured(true); } else { setOidcConfigured(false); } }) .catch((error) => { if (error.response?.status === 404) { setOidcConfigured(false); } else { setOidcConfigured(false); } }); }, []); useEffect(() => { setDbHealthChecking(true); getSetupRequired() .then((res) => { if (res.setup_required) { setFirstUser(true); setTab("signup"); if (!firstUserToastShown) { toast.info(t("auth.firstUserMessage")); setFirstUserToastShown(true); } } else { setFirstUser(false); } setDbError(null); setDbConnectionFailed(false); }) .catch(() => { setDbConnectionFailed(true); }) .finally(() => { setDbHealthChecking(false); }); }, [setDbError, firstUserToastShown]); useEffect(() => { if (!registrationAllowed && !internalLoggedIn) { toast.warning(t("messages.registrationDisabled")); } }, [registrationAllowed, internalLoggedIn, t]); useEffect(() => { if (!passwordLoginAllowed && oidcConfigured && tab !== "external") { setTab("external"); } }, [passwordLoginAllowed, oidcConfigured, tab]); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); if (!localUsername.trim()) { toast.error(t("errors.requiredField")); setLoading(false); return; } if (!passwordLoginAllowed && !firstUser) { toast.error(t("errors.passwordLoginDisabled")); setLoading(false); return; } try { let res; if (tab === "login") { res = await loginUser(localUsername, password); } else { if (password !== signupConfirmPassword) { toast.error(t("errors.passwordMismatch")); setLoading(false); return; } if (password.length < 6) { toast.error(t("errors.minLength", { min: 6 })); setLoading(false); return; } await registerUser(localUsername, password); res = await loginUser(localUsername, password); } if (res.requires_totp) { setTotpRequired(true); setTotpTempToken(res.temp_token); setLoading(false); return; } if (!res || !res.success) { throw new Error(t("errors.loginFailed")); } if (isInElectronWebView() && res.token) { try { localStorage.setItem("jwt", res.token); window.parent.postMessage( { type: "AUTH_SUCCESS", token: res.token, source: "auth_component", platform: "desktop", timestamp: Date.now(), }, "*", ); setWebviewAuthSuccess(true); setLoading(false); return; } catch (e) {} } const [meRes] = await Promise.all([getUserInfo()]); setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); setUserId(meRes.userId || null); setDbError(null); onAuthSuccess({ isAdmin: !!meRes.is_admin, username: meRes.username || null, userId: meRes.userId || null, }); setInternalLoggedIn(true); if (tab === "signup") { setSignupConfirmPassword(""); toast.success(t("messages.registrationSuccess")); } else { toast.success(t("messages.loginSuccess")); } setTotpRequired(false); setTotpCode(""); setTotpTempToken(""); } catch (err: unknown) { const error = err as { message?: string; response?: { data?: { error?: string } }; }; const errorMessage = error?.response?.data?.error || error?.message || t("errors.unknownError"); toast.error(errorMessage); setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); setUsername(null); setUserId(null); if (error?.response?.data?.error?.includes("Database")) { setDbConnectionFailed(true); } else { setDbError(null); } } finally { setLoading(false); } } async function handleInitiatePasswordReset() { setResetLoading(true); try { await initiatePasswordReset(localUsername); setResetStep("verify"); toast.success(t("messages.resetCodeSent")); } catch (err: unknown) { const error = err as { message?: string; response?: { data?: { error?: string } }; }; toast.error( error?.response?.data?.error || error?.message || t("errors.failedPasswordReset"), ); } finally { setResetLoading(false); } } async function handleVerifyResetCode() { setResetLoading(true); try { const response = await verifyPasswordResetCode(localUsername, resetCode); setTempToken(response.tempToken); setResetStep("newPassword"); 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")); } finally { setResetLoading(false); } } async function handleCompletePasswordReset() { setResetLoading(true); if (newPassword !== confirmPassword) { toast.error(t("errors.passwordMismatch")); setResetLoading(false); return; } if (newPassword.length < 6) { toast.error(t("errors.minLength", { min: 6 })); setResetLoading(false); return; } try { await completePasswordReset(localUsername, tempToken, newPassword); setResetStep("initiate"); setResetCode(""); setNewPassword(""); setConfirmPassword(""); setTempToken(""); setResetSuccess(true); toast.success(t("messages.passwordResetSuccess")); setTab("login"); resetPasswordState(); } catch (err: unknown) { const error = err as { response?: { data?: { error?: string } } }; toast.error( error?.response?.data?.error || t("errors.failedCompleteReset"), ); } finally { setResetLoading(false); } } function resetPasswordState() { setResetStep("initiate"); setResetCode(""); setNewPassword(""); setConfirmPassword(""); setTempToken(""); setResetSuccess(false); setSignupConfirmPassword(""); } function clearFormFields() { setPassword(""); setSignupConfirmPassword(""); } async function handleTOTPVerification() { if (totpCode.length !== 6) { toast.error(t("auth.enterCode")); return; } setTotpLoading(true); try { const res = await verifyTOTPLogin(totpTempToken, totpCode); if (!res || !res.success) { throw new Error(t("errors.loginFailed")); } if (isElectron() && res.token) { localStorage.setItem("jwt", res.token); } if (isInElectronWebView() && res.token) { try { localStorage.setItem("jwt", res.token); window.parent.postMessage( { type: "AUTH_SUCCESS", token: res.token, source: "totp_auth_component", platform: "desktop", timestamp: Date.now(), }, "*", ); setWebviewAuthSuccess(true); setTotpLoading(false); return; } catch (e) {} } 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, }); }, 100); setInternalLoggedIn(true); setTotpRequired(false); setTotpCode(""); setTotpTempToken(""); toast.success(t("messages.loginSuccess")); } catch (err: unknown) { const error = err as { message?: string; response?: { data?: { code?: string; error?: string } }; }; const errorCode = error?.response?.data?.code; const errorMessage = error?.response?.data?.error || error?.message || t("errors.invalidTotpCode"); if (errorCode === "SESSION_EXPIRED") { setTotpRequired(false); setTotpCode(""); setTotpTempToken(""); setTab("login"); toast.error(t("errors.sessionExpired")); } else { toast.error(errorMessage); } } finally { setTotpLoading(false); } } async function handleOIDCLogin() { setOidcLoading(true); try { const authResponse = await getOIDCAuthorizeUrl(); const { auth_url: authUrl } = authResponse; if (!authUrl || authUrl === "undefined") { throw new Error(t("errors.invalidAuthUrl")); } window.location.replace(authUrl); } catch (err: unknown) { const error = err as { message?: string; response?: { data?: { error?: string } }; }; const errorMessage = error?.response?.data?.error || error?.message || t("errors.failedOidcLogin"); toast.error(errorMessage); setOidcLoading(false); } } useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const success = urlParams.get("success"); const error = urlParams.get("error"); if (error) { toast.error(`${t("errors.oidcAuthFailed")}: ${error}`); setOidcLoading(false); window.history.replaceState({}, document.title, window.location.pathname); return; } if (success) { setOidcLoading(true); getUserInfo() .then((meRes) => { if (isInElectronWebView()) { const token = getCookie("jwt") || localStorage.getItem("jwt"); if (token) { try { window.parent.postMessage( { type: "AUTH_SUCCESS", token: token, source: "oidc_callback", platform: "desktop", timestamp: Date.now(), }, "*", ); setWebviewAuthSuccess(true); setOidcLoading(false); return; } catch (e) {} } } setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); setUserId(meRes.userId || null); setDbError(null); onAuthSuccess({ isAdmin: !!meRes.is_admin, username: meRes.username || null, userId: meRes.userId || null, }); setInternalLoggedIn(true); window.history.replaceState( {}, document.title, window.location.pathname, ); }) .catch(() => { setInternalLoggedIn(false); setLoggedIn(false); setIsAdmin(false); setUsername(null); setUserId(null); window.history.replaceState( {}, document.title, window.location.pathname, ); }) .finally(() => { setOidcLoading(false); }); } }, []); const Spinner = ( ); const [showServerConfig, setShowServerConfig] = useState( null, ); const [currentServerUrl, setCurrentServerUrl] = useState(""); const [dbConnectionFailed, setDbConnectionFailed] = useState(false); const [dbHealthChecking, setDbHealthChecking] = useState(false); useEffect(() => { if (dbConnectionFailed) { toast.error(t("errors.databaseConnection")); } }, [dbConnectionFailed, t]); useEffect(() => { const checkServerConfig = async () => { if (isInElectronWebView()) { setShowServerConfig(false); return; } if (isElectron()) { try { const config = await getServerConfig(); setCurrentServerUrl(config?.serverUrl || ""); setShowServerConfig(!config || !config.serverUrl); } catch { setShowServerConfig(true); } } else { setShowServerConfig(false); } }; checkServerConfig(); }, []); if (showServerConfig === null && !isInElectronWebView()) { return (
); } if (showServerConfig && !isInElectronWebView()) { return (
{ window.location.reload(); }} onCancel={() => { setShowServerConfig(false); }} isFirstTime={!currentServerUrl} />
); } if ( isElectron() && currentServerUrl && authLoading && !isInElectronWebView() ) { return (
); } if (isElectron() && currentServerUrl && !loggedIn && !isInElectronWebView()) { return (
{ 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")); } }} onChangeServer={() => { setShowServerConfig(true); }} />
); } if (dbHealthChecking && !dbConnectionFailed) { return (

{t("common.checkingDatabase")}

); } if (dbConnectionFailed) { return (

{t("errors.databaseConnection")}

{t("messages.databaseConnectionFailed")}

{isElectron() && currentServerUrl && (
{currentServerUrl}
)}
); } return (
{isInElectronWebView() && !webviewAuthSuccess && ( {t("auth.desktopApp")} {t("auth.loggingInToDesktopApp")} )} {isInElectronWebView() && webviewAuthSuccess && (

{t("messages.loginSuccess")}

{t("auth.redirectingToApp")}

)} {!webviewAuthSuccess && totpRequired && (

{t("auth.twoFactorAuth")}

{t("auth.enterCode")}

setTotpCode(e.target.value.replace(/\D/g, ""))} disabled={totpLoading} className="text-center text-2xl tracking-widest font-mono" autoComplete="one-time-code" />

{t("auth.backupCode")}

)} {!webviewAuthSuccess && !loggedIn && !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")}

); } return ( <>
{passwordLoginAllowed && ( )} {(passwordLoginAllowed || firstUser) && registrationAllowed && ( )} {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")}

{(() => { if (isElectron()) { return (

{t("auth.externalNotSupportedInElectron")}

); } else { return ( ); } })()} )} {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" />
)} )}
) : (
setLocalUsername(e.target.value)} disabled={loading || loggedIn} />
setPassword(e.target.value)} disabled={loading || loggedIn} />
{tab === "signup" && (
setSignupConfirmPassword(e.target.value) } disabled={loading || loggedIn} />
)} {tab === "login" && ( )}
)}
{isElectron() && currentServerUrl && (
{currentServerUrl}
)}
); })()} )}
); }