diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index e0038e2ae..db31c4c17 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1,7 +1,6 @@ import { useContext, useEffect, useState } from "react"; import { Cookies } from "react-cookie"; import { ErrorBoundary } from "react-error-boundary"; -import { useNavigate } from "react-router-dom"; import "reactflow/dist/style.css"; import "./App.css"; import AlertDisplayArea from "./alerts/displayArea"; @@ -11,10 +10,14 @@ import LoadingComponent from "./components/loadingComponent"; import { FETCH_ERROR_DESCRIPION, FETCH_ERROR_MESSAGE, - LANGFLOW_AUTO_LOGIN_OPTION, + LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS, + LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV, } from "./constants/constants"; import { AuthContext } from "./contexts/authContext"; -import { useAutoLogin } from "./controllers/API/queries/auth"; +import { + useAutoLogin, + useRefreshAccessToken, +} from "./controllers/API/queries/auth"; import { useGetHealthQuery } from "./controllers/API/queries/health"; import { useGetGlobalVariables } from "./controllers/API/queries/variables"; import { useGetVersionQuery } from "./controllers/API/queries/version"; @@ -31,21 +34,27 @@ import { useFolderStore } from "./stores/foldersStore"; export default function App() { useTrackLastVisitedPath(); const isLoading = useFlowsManagerStore((state) => state.isLoading); - const { login, setUserData, getUser, logout } = useContext(AuthContext); + const { login, setUserData, getUser } = useContext(AuthContext); const setAutoLogin = useAuthStore((state) => state.setAutoLogin); const setLoading = useAlertStore((state) => state.setLoading); const refreshStars = useDarkStore((state) => state.refreshStars); const dark = useDarkStore((state) => state.dark); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const cookies = new Cookies(); + const logout = useAuthStore((state) => state.logout); + + const refreshToken = cookies.get("refresh_token"); const { mutate: mutateAutoLogin } = useAutoLogin(); useGetVersionQuery(); - const cookies = new Cookies(); const isLoadingFolders = useFolderStore((state) => state.isLoadingFolders); const { mutate: mutateGetGlobalVariables } = useGetGlobalVariables(); + const { mutate: mutateRefresh } = useRefreshAccessToken(); + + const isLoginPage = location.pathname.includes("login"); const { data: healthData, @@ -63,8 +72,6 @@ export default function App() { }, [dark]); useEffect(() => { - const isLoginPage = location.pathname.includes("login"); - mutateAutoLogin(undefined, { onSuccess: async (user) => { if (user && user["access_token"]) { @@ -74,31 +81,45 @@ export default function App() { setUserData(user); setAutoLogin(true); fetchAllData(); + // mutateRefresh({ refresh_token: refreshToken }); } }, onError: (error) => { if (error.name !== "CanceledError") { setAutoLogin(false); - if ( - cookies.get(LANGFLOW_AUTO_LOGIN_OPTION) === "auto" && - isAuthenticated - ) { - logout(); - return; - } - - if (isAuthenticated && !isLoginPage) { - getUser(); - fetchAllData(); - } else { - setLoading(false); - useFlowsManagerStore.setState({ isLoading: false }); + if (!isLoginPage) { + if (!isAuthenticated) { + setLoading(false); + useFlowsManagerStore.setState({ isLoading: false }); + logout(); + } else { + mutateRefresh({ refresh_token: refreshToken }); + fetchAllData(); + getUser(); + } } } }, }); }, []); + useEffect(() => { + const envRefreshTime = LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV; + const automaticRefreshTime = LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS; + + const accessTokenTimer = isNaN(envRefreshTime) + ? automaticRefreshTime + : envRefreshTime; + + const intervalId = setInterval(() => { + if (isAuthenticated && !isLoginPage) { + mutateRefresh({ refresh_token: refreshToken }); + } + }, accessTokenTimer * 1000); + + return () => clearInterval(intervalId); + }, [isLoginPage]); + const fetchAllData = async () => { setTimeout(async () => { await Promise.all([refreshStars(), fetchData()]); diff --git a/src/frontend/src/components/authAdminGuard/index.tsx b/src/frontend/src/components/authAdminGuard/index.tsx index a08e1f4a9..e2966c873 100644 --- a/src/frontend/src/components/authAdminGuard/index.tsx +++ b/src/frontend/src/components/authAdminGuard/index.tsx @@ -4,11 +4,14 @@ import { Navigate } from "react-router-dom"; import { AuthContext } from "../../contexts/authContext"; export const ProtectedAdminRoute = ({ children }) => { - const { logout, userData } = useContext(AuthContext); + const { userData } = useContext(AuthContext); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const autoLogin = useAuthStore((state) => state.autoLogin); const isAdmin = useAuthStore((state) => state.isAdmin); - if (!isAuthenticated) { + const isLoginPage = location.pathname.includes("login"); + const logout = useAuthStore((state) => state.logout); + + if (!isAuthenticated && !isLoginPage) { logout(); } else if ((userData && !isAdmin) || autoLogin) { return ; diff --git a/src/frontend/src/components/authGuard/index.tsx b/src/frontend/src/components/authGuard/index.tsx index 8bfacbd6c..ea3e6f12a 100644 --- a/src/frontend/src/components/authGuard/index.tsx +++ b/src/frontend/src/components/authGuard/index.tsx @@ -1,14 +1,13 @@ import { LANGFLOW_AUTO_LOGIN_OPTION } from "@/constants/constants"; import useAuthStore from "@/stores/authStore"; -import { useContext } from "react"; -import { AuthContext } from "../../contexts/authContext"; export const ProtectedRoute = ({ children }) => { - const { logout } = useContext(AuthContext); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const hasToken = !!localStorage.getItem(LANGFLOW_AUTO_LOGIN_OPTION); + const isLoginPage = location.pathname.includes("login"); + const logout = useAuthStore((state) => state.logout); - if (!isAuthenticated && hasToken) { + if (!isAuthenticated && hasToken && !isLoginPage) { logout(); } else { return children; diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index a4d74462c..7b8663ed9 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -874,3 +874,8 @@ export const TABS_ORDER = [ export const LANGFLOW_ACCESS_TOKEN = "access_token_lf"; export const LANGFLOW_API_TOKEN = "apikey_tkn_lflw"; export const LANGFLOW_AUTO_LOGIN_OPTION = "auto_login_lf"; + +export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS = 60 * 60 - 60 * 60 * 0.1; +export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV = + Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) - + Number(process.env.ACCESS_TOKEN_EXPIRE_SECONDS) * 0.1; diff --git a/src/frontend/src/contexts/authContext.tsx b/src/frontend/src/contexts/authContext.tsx index 735b912bb..52beddf9b 100644 --- a/src/frontend/src/contexts/authContext.tsx +++ b/src/frontend/src/contexts/authContext.tsx @@ -18,7 +18,6 @@ import { AuthContextType } from "../types/contexts/auth"; const initialValue: AuthContextType = { accessToken: null, login: () => {}, - logout: () => new Promise(() => {}), userData: null, setUserData: () => {}, authenticationErrorCount: 0, @@ -90,29 +89,6 @@ export function AuthProvider({ children }): React.ReactElement { getUser(); } - async function logout() { - if (autoLogin) { - return; - } - try { - await requestLogout(); - cookies.remove(LANGFLOW_API_TOKEN, { path: "/" }); - cookies.remove(LANGFLOW_AUTO_LOGIN_OPTION, { path: "/" }); - setIsAdmin(false); - setUserData(null); - setAccessToken(null); - setIsAuthenticated(false); - setIsAuthenticated(false); - setAllFlows([]); - setSelectedFolder(null); - navigate("/login"); - setIsLoading(false); - } catch (error) { - console.error(error); - throw error; - } - } - function storeApiKey(apikey: string) { setApiKey(apikey); } @@ -123,7 +99,6 @@ export function AuthProvider({ children }): React.ReactElement { value={{ accessToken, login, - logout, setUserData, userData, authenticationErrorCount: 0, diff --git a/src/frontend/src/controllers/API/api.tsx b/src/frontend/src/controllers/API/api.tsx index 66b7334ae..1a4156a1b 100644 --- a/src/frontend/src/controllers/API/api.tsx +++ b/src/frontend/src/controllers/API/api.tsx @@ -25,6 +25,7 @@ function ApiInterceptor() { const { mutate: mutationLogout } = useLogout(); const { mutate: mutationRenewAccessToken } = useRefreshAccessToken(); const logout = useAuthStore((state) => state.logout); + const isLoginPage = location.pathname.includes("login"); useEffect(() => { const interceptor = api.interceptors.response.use( @@ -122,6 +123,8 @@ function ApiInterceptor() { }, [accessToken, setErrorData]); function checkErrorCount() { + if (isLoginPage) return; + authenticationErrorCount = authenticationErrorCount + 1; if (authenticationErrorCount > 3) { @@ -141,7 +144,7 @@ function ApiInterceptor() { } async function tryToRenewAccessToken(error: AxiosError) { - if (window.location.pathname.includes("/login")) return; + if (isLoginPage) return; mutationRenewAccessToken( {}, { diff --git a/src/frontend/src/controllers/API/queries/auth/use-post-logout.ts b/src/frontend/src/controllers/API/queries/auth/use-post-logout.ts index df0005f57..80aa0c15a 100644 --- a/src/frontend/src/controllers/API/queries/auth/use-post-logout.ts +++ b/src/frontend/src/controllers/API/queries/auth/use-post-logout.ts @@ -1,11 +1,6 @@ import useAuthStore from "@/stores/authStore"; -import { - changeUser, - resetPasswordType, - useMutationFunctionType, -} from "@/types/api"; -import { UseMutationResult } from "@tanstack/react-query"; -import { useNavigate } from "react-router-dom"; +import { useMutationFunctionType } from "@/types/api"; + import { api } from "../../api"; import { getURL } from "../../helpers/constants"; import { UseRequestProcessor } from "../../services/request-processor"; @@ -14,7 +9,6 @@ export const useLogout: useMutationFunctionType = ( options?, ) => { const { mutate } = UseRequestProcessor(); - const navigate = useNavigate(); const logout = useAuthStore((state) => state.logout); async function logoutUser(): Promise { @@ -29,7 +23,6 @@ export const useLogout: useMutationFunctionType = ( const mutation = mutate(["useLogout"], logoutUser, { onSuccess: () => { logout(); - navigate("/login"); }, onError: (error) => { console.error(error); diff --git a/src/frontend/src/pages/AdminPage/LoginPage/index.tsx b/src/frontend/src/pages/AdminPage/LoginPage/index.tsx index e0119ca6a..442126e3c 100644 --- a/src/frontend/src/pages/AdminPage/LoginPage/index.tsx +++ b/src/frontend/src/pages/AdminPage/LoginPage/index.tsx @@ -1,5 +1,7 @@ import { useLoginUser } from "@/controllers/API/queries/auth"; import { useGetGlobalVariables } from "@/controllers/API/queries/variables"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; +import { useFolderStore } from "@/stores/foldersStore"; import { useContext, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "../../../components/ui/button"; @@ -24,6 +26,9 @@ export default function LoginAdminPage() { const { mutate: mutateGetGlobalVariables } = useGetGlobalVariables(); + const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows); + const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder); + const { password, username } = inputState; const setErrorData = useAlertStore((state) => state.setErrorData); function handleInput({ @@ -42,6 +47,9 @@ export default function LoginAdminPage() { mutate(user, { onSuccess: (res) => { + setAllFlows([]); + setSelectedFolder(null); + setLoading(true); login(res.access_token, "login"); diff --git a/src/frontend/src/pages/LoginPage/index.tsx b/src/frontend/src/pages/LoginPage/index.tsx index 7c8fece04..ac526da2b 100644 --- a/src/frontend/src/pages/LoginPage/index.tsx +++ b/src/frontend/src/pages/LoginPage/index.tsx @@ -1,4 +1,5 @@ import { useLoginUser } from "@/controllers/API/queries/auth"; +import { useFolderStore } from "@/stores/foldersStore"; import * as Form from "@radix-ui/react-form"; import { useContext, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; @@ -25,6 +26,8 @@ export default function LoginPage(): JSX.Element { const navigate = useNavigate(); const setErrorData = useAlertStore((state) => state.setErrorData); const setLoading = useFlowsManagerStore((state) => state.setIsLoading); + const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows); + const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder); function handleInput({ target: { name, value }, @@ -42,7 +45,9 @@ export default function LoginPage(): JSX.Element { mutate(user, { onSuccess: (data) => { - console.log("admin page"); + setAllFlows([]); + setSelectedFolder(null); + setLoading(true); login(data.access_token, "login"); navigate("/admin/"); diff --git a/src/frontend/src/stores/authStore.ts b/src/frontend/src/stores/authStore.ts index 1d53d8cdc..4dcd917c1 100644 --- a/src/frontend/src/stores/authStore.ts +++ b/src/frontend/src/stores/authStore.ts @@ -26,9 +26,9 @@ const useAuthStore = create((set, get) => ({ set({ authenticationErrorCount }), logout: async () => { - const setAllFlows = useFlowsManagerStore.getState().setAllFlows; - const setSelectedFolder = useFolderStore.getState().setSelectedFolder; - cookies.remove("apikey_tkn_lflw", { path: "/" }); + get().setIsAuthenticated(false); + get().setIsAdmin(false); + set({ isAdmin: false, userData: null, @@ -37,8 +37,8 @@ const useAuthStore = create((set, get) => ({ autoLogin: false, apiKey: null, }); - setAllFlows([]); - setSelectedFolder(null); + + window.location.href = "/login"; }, // getUser: () => { // const setLoading = useAlertStore.getState().setLoading; diff --git a/src/frontend/src/types/contexts/auth.ts b/src/frontend/src/types/contexts/auth.ts index 32dbef825..8bbaac0aa 100644 --- a/src/frontend/src/types/contexts/auth.ts +++ b/src/frontend/src/types/contexts/auth.ts @@ -3,7 +3,6 @@ import { Users } from "../api"; export type AuthContextType = { accessToken: string | null; login: (accessToken: string, autoLogin: string) => void; - logout: () => Promise; userData: Users | null; setUserData: (userData: Users | null) => void; authenticationErrorCount: number; diff --git a/src/frontend/vite.config.mts b/src/frontend/vite.config.mts index c52b05777..24e4c1795 100644 --- a/src/frontend/vite.config.mts +++ b/src/frontend/vite.config.mts @@ -32,6 +32,10 @@ export default defineConfig(({ mode }) => { }, define: { "process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL), + "process.env.ACCESS_TOKEN_EXPIRE_SECONDS": JSON.stringify( + process.env.ACCESS_TOKEN_EXPIRE_SECONDS, + ), + "process.env.CI": JSON.stringify(process.env.CI), }, plugins: [react(), svgr(), tsconfigPaths()], server: {