import React, { useState, useEffect, useRef, useCallback } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; import { useTranslation } from "react-i18next"; import { AlertCircle, Loader2, ArrowLeft, RefreshCw } from "lucide-react"; interface ElectronLoginFormProps { serverUrl: string; onAuthSuccess: (previousJwt: string | null) => void | Promise; onChangeServer: () => void; } const AUTH_MESSAGE_SOURCES = new Set([ "auth_component", "totp_auth_component", "oidc_callback", ]); export function ElectronLoginForm({ serverUrl, onAuthSuccess, onChangeServer, }: ElectronLoginFormProps) { const { t } = useTranslation(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isAuthenticating, setIsAuthenticating] = useState(false); const isAuthenticatingRef = useRef(false); const iframeRef = useRef(null); const hasAuthenticatedRef = useRef(false); const [cookieSnapshotReady, setCookieSnapshotReady] = useState(false); const [currentUrl, setCurrentUrl] = useState(serverUrl); const hasLoadedOnce = useRef(false); const onAuthSuccessRef = useRef(onAuthSuccess); const initialJwtRef = useRef(undefined); useEffect(() => { onAuthSuccessRef.current = onAuthSuccess; }, [onAuthSuccess]); useEffect(() => { window.electronAPI ?.getSessionCookie?.("jwt", serverUrl) .then((value) => { initialJwtRef.current = value; }) .catch(() => { initialJwtRef.current = null; }) .finally(() => { setCookieSnapshotReady(true); }); }, [serverUrl]); const handleAuthSuccess = useCallback(async () => { if (hasAuthenticatedRef.current || isAuthenticatingRef.current) return; hasAuthenticatedRef.current = true; isAuthenticatingRef.current = true; setIsAuthenticating(true); try { await onAuthSuccessRef.current(initialJwtRef.current ?? null); } catch (_err) { setError(t("errors.authTokenSaveFailed")); isAuthenticatingRef.current = false; setIsAuthenticating(false); hasAuthenticatedRef.current = false; } }, [t]); // postMessage from server Auth.tsx after the backend has set the HttpOnly cookie. useEffect(() => { const handleMessage = async (event: MessageEvent) => { try { const expectedOrigin = new URL(serverUrl).origin; if (event.origin !== expectedOrigin) return; if (event.source !== iframeRef.current?.contentWindow) return; if (!event.data || typeof event.data !== "object") return; const { type, platform, source } = event.data; if ( type === "AUTH_SUCCESS" && platform === "desktop" && AUTH_MESSAGE_SOURCES.has(source) ) { await handleAuthSuccess(); } } catch { // ignore } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, [handleAuthSuccess, serverUrl]); useEffect(() => { const iframe = iframeRef.current; if (!iframe) return; const handleLoad = () => { setLoading(false); hasLoadedOnce.current = true; setError(null); try { if (iframe.contentWindow) { setCurrentUrl(iframe.contentWindow.location.href); } } catch { setCurrentUrl(serverUrl); } }; const handleError = () => { setLoading(false); if (hasLoadedOnce.current) { setError(t("errors.failedToLoadServer")); } }; iframe.addEventListener("load", handleLoad); iframe.addEventListener("error", handleError); return () => { iframe.removeEventListener("load", handleLoad); iframe.removeEventListener("error", handleError); }; }, [serverUrl, t]); const handleRefresh = () => { if (iframeRef.current) { iframeRef.current.src = serverUrl; setLoading(true); setError(null); } }; const displayUrl = currentUrl.replace(/^https?:\/\//, ""); const isEmbeddedServer = serverUrl.includes("localhost:30001"); return (
{isAuthenticating && (
)} {!isAuthenticating && (
{!isEmbeddedServer && (
{displayUrl}
)} {isEmbeddedServer &&
}
)} {error && !isAuthenticating && (
{t("common.error")} {error}
)} {loading && !isAuthenticating && (
{t("auth.loadingServer")}
)}