diff --git a/src/frontend/src/components/authorization/authGuard/index.tsx b/src/frontend/src/components/authorization/authGuard/index.tsx index f01529301..263281c84 100644 --- a/src/frontend/src/components/authorization/authGuard/index.tsx +++ b/src/frontend/src/components/authorization/authGuard/index.tsx @@ -1,4 +1,5 @@ import { + IS_AUTO_LOGIN, LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS, LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV, } from "@/constants/constants"; @@ -11,6 +12,14 @@ export const ProtectedRoute = ({ children }) => { const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const { mutate: mutateRefresh } = useRefreshAccessToken(); const autoLogin = useAuthStore((state) => state.autoLogin); + const isAutoLoginEnv = IS_AUTO_LOGIN; + const testMockAutoLogin = sessionStorage.getItem("testMockAutoLogin"); + + const shouldRedirect = + !isAuthenticated && + autoLogin !== undefined && + !autoLogin && + !isAutoLoginEnv; useEffect(() => { const envRefreshTime = LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV; @@ -30,7 +39,8 @@ export const ProtectedRoute = ({ children }) => { return () => clearInterval(intervalId); } }, [isAuthenticated]); - if (!isAuthenticated && autoLogin !== undefined && !autoLogin) { + + if (shouldRedirect || testMockAutoLogin) { const currentPath = window.location.pathname; const isHomePath = currentPath === "/" || currentPath === "/flows"; const isLoginPage = location.pathname.includes("login"); diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 0b08a88cd..2d12b941a 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -1009,4 +1009,11 @@ export const POLLING_MESSAGES = { STREAMING_NOT_SUPPORTED: "Streaming not supported", } as const; -export const POLLING_INTERVAL = 100; // milliseconds between polling attempts +export const POLLING_INTERVAL = 100; + +export const IS_AUTO_LOGIN = + !process?.env?.LANGFLOW_AUTO_LOGIN || + String(process?.env?.LANGFLOW_AUTO_LOGIN)?.toLowerCase() !== "false"; + +export const AUTO_LOGIN_RETRY_DELAY = 2000; +export const AUTO_LOGIN_MAX_RETRY_DELAY = 60000; diff --git a/src/frontend/src/controllers/API/api.tsx b/src/frontend/src/controllers/API/api.tsx index 9c23b490f..2fc6a886c 100644 --- a/src/frontend/src/controllers/API/api.tsx +++ b/src/frontend/src/controllers/API/api.tsx @@ -1,4 +1,4 @@ -import { LANGFLOW_ACCESS_TOKEN } from "@/constants/constants"; +import { IS_AUTO_LOGIN, LANGFLOW_ACCESS_TOKEN } from "@/constants/constants"; import { useCustomApiHeaders } from "@/customization/hooks/use-custom-api-headers"; import useAuthStore from "@/stores/authStore"; import { useUtilityStore } from "@/stores/utilityStore"; @@ -65,7 +65,7 @@ function ApiInterceptor() { const isAuthenticationError = error?.response?.status === 403 || error?.response?.status === 401; - if (isAuthenticationError) { + if (isAuthenticationError && !IS_AUTO_LOGIN) { if (autoLogin !== undefined && !autoLogin) { if (error?.config?.url?.includes("github")) { return Promise.reject(error); diff --git a/src/frontend/src/controllers/API/queries/auth/use-get-autologin.ts b/src/frontend/src/controllers/API/queries/auth/use-get-autologin.ts index e5b9888a1..d6ced0565 100644 --- a/src/frontend/src/controllers/API/queries/auth/use-get-autologin.ts +++ b/src/frontend/src/controllers/API/queries/auth/use-get-autologin.ts @@ -1,8 +1,13 @@ +import { + AUTO_LOGIN_MAX_RETRY_DELAY, + AUTO_LOGIN_RETRY_DELAY, + IS_AUTO_LOGIN, +} from "@/constants/constants"; import { AuthContext } from "@/contexts/authContext"; import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; import useAuthStore from "@/stores/authStore"; import { AxiosError } from "axios"; -import { useContext } from "react"; +import { useContext, useRef } from "react"; import { useQueryFunctionType, Users } from "../../../../types/api"; import { api } from "../../api"; import { getURL } from "../../helpers/constants"; @@ -27,6 +32,9 @@ export const useGetAutoLogin: useQueryFunctionType = ( const navigate = useCustomNavigate(); const { mutateAsync: mutationLogout } = useLogout(); + const retryCountRef = useRef(0); + const retryTimerRef = useRef(null); + async function getAutoLoginFn(): Promise { try { const response = await api.get(`${getURL("AUTOLOGIN")}`); @@ -36,29 +44,61 @@ export const useGetAutoLogin: useQueryFunctionType = ( login(user["access_token"], "auto"); setUserData(user); setAutoLogin(true); + resetTimer(); } } catch (e) { const error = e as AxiosError; if (error.name !== "CanceledError") { setAutoLogin(false); if (!isLoginPage) { - if (!isAuthenticated) { - await mutationLogout(); - const currentPath = window.location.pathname; - const isHomePath = currentPath === "/" || currentPath === "/flows"; - navigate( - "/login" + - (!isHomePath && !isLoginPage ? "?redirect=" + currentPath : ""), - ); - } else { - getUser(); - } + await handleAutoLoginError(); } } } return null; } + const resetTimer = () => { + retryCountRef.current = 0; + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current); + retryTimerRef.current = null; + } + }; + + const handleAutoLoginError = async () => { + const manualLoginNotAuthenticated = !isAuthenticated && !IS_AUTO_LOGIN; + const autoLoginNotAuthenticated = !isAuthenticated && IS_AUTO_LOGIN; + + if (manualLoginNotAuthenticated) { + await mutationLogout(); + const currentPath = window.location.pathname; + const isHomePath = currentPath === "/" || currentPath === "/flows"; + navigate( + "/login" + + (!isHomePath && !isLoginPage ? "?redirect=" + currentPath : ""), + ); + } else if (autoLoginNotAuthenticated) { + const retryCount = retryCountRef.current; + const delay = Math.min( + AUTO_LOGIN_RETRY_DELAY * Math.pow(2, retryCount), + AUTO_LOGIN_MAX_RETRY_DELAY, + ); + + retryCountRef.current += 1; + + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current); + } + + retryTimerRef.current = setTimeout(() => { + getAutoLoginFn(); + }, delay); + } else { + getUser(); + } + }; + const queryResult = query(["useGetAutoLogin"], getAutoLoginFn, { refetchOnWindowFocus: false, ...options, diff --git a/src/frontend/src/controllers/API/queries/auth/use-post-refresh-access.ts b/src/frontend/src/controllers/API/queries/auth/use-post-refresh-access.ts index a72d810ee..03c767cc0 100644 --- a/src/frontend/src/controllers/API/queries/auth/use-post-refresh-access.ts +++ b/src/frontend/src/controllers/API/queries/auth/use-post-refresh-access.ts @@ -1,4 +1,4 @@ -import { LANGFLOW_REFRESH_TOKEN } from "@/constants/constants"; +import { IS_AUTO_LOGIN, LANGFLOW_REFRESH_TOKEN } from "@/constants/constants"; import { useMutationFunctionType } from "@/types/api"; import { Cookies } from "react-cookie"; import { api } from "../../api"; @@ -25,7 +25,10 @@ export const useRefreshAccessToken: useMutationFunctionType< return res.data; } - const mutation = mutate(["useRefreshAccessToken"], refreshAccess, options); + const mutation = mutate(["useRefreshAccessToken"], refreshAccess, { + ...options, + retry: IS_AUTO_LOGIN ? 0 : 3, + }); return mutation; }; diff --git a/src/frontend/src/controllers/API/services/request-processor.ts b/src/frontend/src/controllers/API/services/request-processor.ts index 49fb9e9ea..c54f445f1 100644 --- a/src/frontend/src/controllers/API/services/request-processor.ts +++ b/src/frontend/src/controllers/API/services/request-processor.ts @@ -23,6 +23,8 @@ export function UseRequestProcessor(): { return useQuery({ queryKey, queryFn, + retry: 5, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), ...options, }); } @@ -40,6 +42,8 @@ export function UseRequestProcessor(): { options.onSettled && options.onSettled(data, error, variables, context); }, ...options, + retry: options.retry ?? 3, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }); } diff --git a/src/frontend/tests/core/features/auto-login-off.spec.ts b/src/frontend/tests/core/features/auto-login-off.spec.ts index d7a4bdbdb..cc6b94203 100644 --- a/src/frontend/tests/core/features/auto-login-off.spec.ts +++ b/src/frontend/tests/core/features/auto-login-off.spec.ts @@ -15,6 +15,20 @@ test( }); }); + await page.addInitScript(() => { + window.process = window.process || {}; + + const newEnv = { ...window.process.env, LANGFLOW_AUTO_LOGIN: "false" }; + + Object.defineProperty(window.process, "env", { + value: newEnv, + writable: true, + configurable: true, + }); + + sessionStorage.setItem("testMockAutoLogin", "true"); + }); + const randomName = Math.random().toString(36).substring(5); const randomPassword = Math.random().toString(36).substring(5); const secondRandomName = Math.random().toString(36).substring(5); @@ -28,6 +42,10 @@ test( await page.getByPlaceholder("Username").fill("langflow"); await page.getByPlaceholder("Password").fill("langflow"); + await page.evaluate(() => { + sessionStorage.removeItem("testMockAutoLogin"); + }); + await page.getByRole("button", { name: "Sign In" }).click(); await page.waitForSelector('[data-testid="mainpage_title"]', { @@ -161,6 +179,10 @@ test( await page.getByTestId("user-profile-settings").click(); + await page.evaluate(() => { + sessionStorage.setItem("testMockAutoLogin", "true"); + }); + await page.getByText("Logout", { exact: true }).click(); await page.waitForSelector("text=sign in to langflow", { timeout: 30000 }); @@ -174,6 +196,10 @@ test( await page.getByRole("button", { name: "Sign In" }).click(); + await page.evaluate(() => { + sessionStorage.removeItem("testMockAutoLogin"); + }); + await page.waitForSelector('[id="new-project-btn"]', { timeout: 30000, }); @@ -231,6 +257,10 @@ test( await page.getByTestId("user-profile-settings").click(); + await page.evaluate(() => { + sessionStorage.setItem("testMockAutoLogin", "true"); + }); + await page.getByText("Logout", { exact: true }).click(); await page.waitForSelector("text=sign in to langflow", { timeout: 30000 }); @@ -238,6 +268,10 @@ test( await page.getByPlaceholder("Username").fill("langflow"); await page.getByPlaceholder("Password").fill("langflow"); + await page.evaluate(() => { + sessionStorage.removeItem("testMockAutoLogin"); + }); + await page.getByRole("button", { name: "Sign In" }).click(); await page.waitForSelector('[data-testid="mainpage_title"]', { @@ -255,5 +289,9 @@ test( await expect(page.getByText(randomFlowName, { exact: true })).toBeVisible({ timeout: 2000, }); + + await page.evaluate(() => { + sessionStorage.removeItem("testMockAutoLogin"); + }); }, ); diff --git a/src/frontend/vite.config.mts b/src/frontend/vite.config.mts index 5296c259e..3634e122a 100644 --- a/src/frontend/vite.config.mts +++ b/src/frontend/vite.config.mts @@ -1,4 +1,6 @@ import react from "@vitejs/plugin-react-swc"; +import * as dotenv from "dotenv"; +import path from "path"; import { defineConfig, loadEnv } from "vite"; import svgr from "vite-plugin-svgr"; import tsconfigPaths from "vite-tsconfig-paths"; @@ -12,6 +14,12 @@ import { export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); + const envLangflowResult = dotenv.config({ + path: path.resolve(__dirname, "../../.env"), + }); + + const envLangflow = envLangflowResult.parsed || {}; + const apiRoutes = API_ROUTES || ["^/api/v1/", "/health"]; const target = @@ -35,11 +43,16 @@ export default defineConfig(({ mode }) => { outDir: "build", }, define: { - "process.env.BACKEND_URL": JSON.stringify(env.BACKEND_URL), - "process.env.ACCESS_TOKEN_EXPIRE_SECONDS": JSON.stringify( - env.ACCESS_TOKEN_EXPIRE_SECONDS, + "process.env.BACKEND_URL": JSON.stringify( + envLangflow.BACKEND_URL ?? "http://127.0.0.1:7860", + ), + "process.env.ACCESS_TOKEN_EXPIRE_SECONDS": JSON.stringify( + envLangflow.ACCESS_TOKEN_EXPIRE_SECONDS ?? 60, + ), + "process.env.CI": JSON.stringify(envLangflow.CI ?? false), + "process.env.LANGFLOW_AUTO_LOGIN": JSON.stringify( + envLangflow.LANGFLOW_AUTO_LOGIN ?? true, ), - "process.env.CI": JSON.stringify(env.CI), }, plugins: [react(), svgr(), tsconfigPaths()], server: {