bugfix: automatically refresh user's session (#3148)

📝 (App.tsx): Update imports and add new constants for access token expiration time
🔧 (App.tsx): Add support for refreshing access token and setting expiration time
📝 (authAdminGuard/index.tsx): Remove unnecessary logout function and add conditional check for login page
📝 (authGuard/index.tsx): Add conditional check for login page
📝 (constants.ts): Add constants for access token expiration time
📝 (authContext.tsx): Remove logout function and update related logic
📝 (api.tsx): Add conditional check for login page
📝 (use-post-logout.ts): Remove navigate function call
📝 (LoginPage/index.tsx): Add logic to reset flows and selected folder on successful login

✨ (LoginPage/index.tsx): Add support for managing folders in the login page to improve user experience
🔧 (authStore.ts): Update logout function to clear user data and redirect to login page for better security and user experience
📝 (contexts/auth.ts): Remove unnecessary logout function from AuthContextType for consistency
🔧 (vite.config.mts): Add environment variables for ACCESS_TOKEN_EXPIRE_SECONDS and CI to improve configuration flexibility

Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2024-08-01 16:45:32 -03:00 • committed by GitHub
commit 7c75ad0728
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 84 additions and 69 deletions

View file

@ -1,7 +1,6 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { Cookies } from "react-cookie"; import { Cookies } from "react-cookie";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { useNavigate } from "react-router-dom";
import "reactflow/dist/style.css"; import "reactflow/dist/style.css";
import "./App.css"; import "./App.css";
import AlertDisplayArea from "./alerts/displayArea"; import AlertDisplayArea from "./alerts/displayArea";
@ -11,10 +10,14 @@ import LoadingComponent from "./components/loadingComponent";
import { import {
FETCH_ERROR_DESCRIPION, FETCH_ERROR_DESCRIPION,
FETCH_ERROR_MESSAGE, FETCH_ERROR_MESSAGE,
LANGFLOW_AUTO_LOGIN_OPTION, LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS,
LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV,
} from "./constants/constants"; } from "./constants/constants";
import { AuthContext } from "./contexts/authContext"; 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 { useGetHealthQuery } from "./controllers/API/queries/health";
import { useGetGlobalVariables } from "./controllers/API/queries/variables"; import { useGetGlobalVariables } from "./controllers/API/queries/variables";
import { useGetVersionQuery } from "./controllers/API/queries/version"; import { useGetVersionQuery } from "./controllers/API/queries/version";
@ -31,21 +34,27 @@ import { useFolderStore } from "./stores/foldersStore";
export default function App() { export default function App() {
useTrackLastVisitedPath(); useTrackLastVisitedPath();
const isLoading = useFlowsManagerStore((state) => state.isLoading); 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 setAutoLogin = useAuthStore((state) => state.setAutoLogin);
const setLoading = useAlertStore((state) => state.setLoading); const setLoading = useAlertStore((state) => state.setLoading);
const refreshStars = useDarkStore((state) => state.refreshStars); const refreshStars = useDarkStore((state) => state.refreshStars);
const dark = useDarkStore((state) => state.dark); const dark = useDarkStore((state) => state.dark);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated); 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(); const { mutate: mutateAutoLogin } = useAutoLogin();
useGetVersionQuery(); useGetVersionQuery();
const cookies = new Cookies();
const isLoadingFolders = useFolderStore((state) => state.isLoadingFolders); const isLoadingFolders = useFolderStore((state) => state.isLoadingFolders);
const { mutate: mutateGetGlobalVariables } = useGetGlobalVariables(); const { mutate: mutateGetGlobalVariables } = useGetGlobalVariables();
const { mutate: mutateRefresh } = useRefreshAccessToken();
const isLoginPage = location.pathname.includes("login");
const { const {
data: healthData, data: healthData,
@ -63,8 +72,6 @@ export default function App() {
}, [dark]); }, [dark]);
useEffect(() => { useEffect(() => {
const isLoginPage = location.pathname.includes("login");
mutateAutoLogin(undefined, { mutateAutoLogin(undefined, {
onSuccess: async (user) => { onSuccess: async (user) => {
if (user && user["access_token"]) { if (user && user["access_token"]) {
@ -74,31 +81,45 @@ export default function App() {
setUserData(user); setUserData(user);
setAutoLogin(true); setAutoLogin(true);
fetchAllData(); fetchAllData();
// mutateRefresh({ refresh_token: refreshToken });
} }
}, },
onError: (error) => { onError: (error) => {
if (error.name !== "CanceledError") { if (error.name !== "CanceledError") {
setAutoLogin(false); setAutoLogin(false);
if ( if (!isLoginPage) {
cookies.get(LANGFLOW_AUTO_LOGIN_OPTION) === "auto" && if (!isAuthenticated) {
isAuthenticated setLoading(false);
) { useFlowsManagerStore.setState({ isLoading: false });
logout(); logout();
return; } else {
} mutateRefresh({ refresh_token: refreshToken });
fetchAllData();
if (isAuthenticated && !isLoginPage) { getUser();
getUser(); }
fetchAllData();
} else {
setLoading(false);
useFlowsManagerStore.setState({ isLoading: false });
} }
} }
}, },
}); });
}, []); }, []);
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 () => { const fetchAllData = async () => {
setTimeout(async () => { setTimeout(async () => {
await Promise.all([refreshStars(), fetchData()]); await Promise.all([refreshStars(), fetchData()]);

View file

@ -4,11 +4,14 @@ import { Navigate } from "react-router-dom";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
export const ProtectedAdminRoute = ({ children }) => { export const ProtectedAdminRoute = ({ children }) => {
const { logout, userData } = useContext(AuthContext); const { userData } = useContext(AuthContext);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const autoLogin = useAuthStore((state) => state.autoLogin); const autoLogin = useAuthStore((state) => state.autoLogin);
const isAdmin = useAuthStore((state) => state.isAdmin); const isAdmin = useAuthStore((state) => state.isAdmin);
if (!isAuthenticated) { const isLoginPage = location.pathname.includes("login");
const logout = useAuthStore((state) => state.logout);
if (!isAuthenticated && !isLoginPage) {
logout(); logout();
} else if ((userData && !isAdmin) || autoLogin) { } else if ((userData && !isAdmin) || autoLogin) {
return <Navigate to="/" replace />; return <Navigate to="/" replace />;

View file

@ -1,14 +1,13 @@
import { LANGFLOW_AUTO_LOGIN_OPTION } from "@/constants/constants"; import { LANGFLOW_AUTO_LOGIN_OPTION } from "@/constants/constants";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import { useContext } from "react";
import { AuthContext } from "../../contexts/authContext";
export const ProtectedRoute = ({ children }) => { export const ProtectedRoute = ({ children }) => {
const { logout } = useContext(AuthContext);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const hasToken = !!localStorage.getItem(LANGFLOW_AUTO_LOGIN_OPTION); 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(); logout();
} else { } else {
return children; return children;

View file

@ -874,3 +874,8 @@ export const TABS_ORDER = [
export const LANGFLOW_ACCESS_TOKEN = "access_token_lf"; export const LANGFLOW_ACCESS_TOKEN = "access_token_lf";
export const LANGFLOW_API_TOKEN = "apikey_tkn_lflw"; export const LANGFLOW_API_TOKEN = "apikey_tkn_lflw";
export const LANGFLOW_AUTO_LOGIN_OPTION = "auto_login_lf"; 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;

View file

@ -18,7 +18,6 @@ import { AuthContextType } from "../types/contexts/auth";
const initialValue: AuthContextType = { const initialValue: AuthContextType = {
accessToken: null, accessToken: null,
login: () => {}, login: () => {},
logout: () => new Promise(() => {}),
userData: null, userData: null,
setUserData: () => {}, setUserData: () => {},
authenticationErrorCount: 0, authenticationErrorCount: 0,
@ -90,29 +89,6 @@ export function AuthProvider({ children }): React.ReactElement {
getUser(); 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) { function storeApiKey(apikey: string) {
setApiKey(apikey); setApiKey(apikey);
} }
@ -123,7 +99,6 @@ export function AuthProvider({ children }): React.ReactElement {
value={{ value={{
accessToken, accessToken,
login, login,
logout,
setUserData, setUserData,
userData, userData,
authenticationErrorCount: 0, authenticationErrorCount: 0,

View file

@ -25,6 +25,7 @@ function ApiInterceptor() {
const { mutate: mutationLogout } = useLogout(); const { mutate: mutationLogout } = useLogout();
const { mutate: mutationRenewAccessToken } = useRefreshAccessToken(); const { mutate: mutationRenewAccessToken } = useRefreshAccessToken();
const logout = useAuthStore((state) => state.logout); const logout = useAuthStore((state) => state.logout);
const isLoginPage = location.pathname.includes("login");
useEffect(() => { useEffect(() => {
const interceptor = api.interceptors.response.use( const interceptor = api.interceptors.response.use(
@ -122,6 +123,8 @@ function ApiInterceptor() {
}, [accessToken, setErrorData]); }, [accessToken, setErrorData]);
function checkErrorCount() { function checkErrorCount() {
if (isLoginPage) return;
authenticationErrorCount = authenticationErrorCount + 1; authenticationErrorCount = authenticationErrorCount + 1;
if (authenticationErrorCount > 3) { if (authenticationErrorCount > 3) {
@ -141,7 +144,7 @@ function ApiInterceptor() {
} }
async function tryToRenewAccessToken(error: AxiosError) { async function tryToRenewAccessToken(error: AxiosError) {
if (window.location.pathname.includes("/login")) return; if (isLoginPage) return;
mutationRenewAccessToken( mutationRenewAccessToken(
{}, {},
{ {

View file

@ -1,11 +1,6 @@
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import { import { useMutationFunctionType } from "@/types/api";
changeUser,
resetPasswordType,
useMutationFunctionType,
} from "@/types/api";
import { UseMutationResult } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { api } from "../../api"; import { api } from "../../api";
import { getURL } from "../../helpers/constants"; import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor"; import { UseRequestProcessor } from "../../services/request-processor";
@ -14,7 +9,6 @@ export const useLogout: useMutationFunctionType<undefined, void> = (
options?, options?,
) => { ) => {
const { mutate } = UseRequestProcessor(); const { mutate } = UseRequestProcessor();
const navigate = useNavigate();
const logout = useAuthStore((state) => state.logout); const logout = useAuthStore((state) => state.logout);
async function logoutUser(): Promise<any> { async function logoutUser(): Promise<any> {
@ -29,7 +23,6 @@ export const useLogout: useMutationFunctionType<undefined, void> = (
const mutation = mutate(["useLogout"], logoutUser, { const mutation = mutate(["useLogout"], logoutUser, {
onSuccess: () => { onSuccess: () => {
logout(); logout();
navigate("/login");
}, },
onError: (error) => { onError: (error) => {
console.error(error); console.error(error);

View file

@ -1,5 +1,7 @@
import { useLoginUser } from "@/controllers/API/queries/auth"; import { useLoginUser } from "@/controllers/API/queries/auth";
import { useGetGlobalVariables } from "@/controllers/API/queries/variables"; import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useFolderStore } from "@/stores/foldersStore";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Button } from "../../../components/ui/button"; import { Button } from "../../../components/ui/button";
@ -24,6 +26,9 @@ export default function LoginAdminPage() {
const { mutate: mutateGetGlobalVariables } = useGetGlobalVariables(); const { mutate: mutateGetGlobalVariables } = useGetGlobalVariables();
const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows);
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
const { password, username } = inputState; const { password, username } = inputState;
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
function handleInput({ function handleInput({
@ -42,6 +47,9 @@ export default function LoginAdminPage() {
mutate(user, { mutate(user, {
onSuccess: (res) => { onSuccess: (res) => {
setAllFlows([]);
setSelectedFolder(null);
setLoading(true); setLoading(true);
login(res.access_token, "login"); login(res.access_token, "login");

View file

@ -1,4 +1,5 @@
import { useLoginUser } from "@/controllers/API/queries/auth"; import { useLoginUser } from "@/controllers/API/queries/auth";
import { useFolderStore } from "@/stores/foldersStore";
import * as Form from "@radix-ui/react-form"; import * as Form from "@radix-ui/react-form";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
@ -25,6 +26,8 @@ export default function LoginPage(): JSX.Element {
const navigate = useNavigate(); const navigate = useNavigate();
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const setLoading = useFlowsManagerStore((state) => state.setIsLoading); const setLoading = useFlowsManagerStore((state) => state.setIsLoading);
const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows);
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
function handleInput({ function handleInput({
target: { name, value }, target: { name, value },
@ -42,7 +45,9 @@ export default function LoginPage(): JSX.Element {
mutate(user, { mutate(user, {
onSuccess: (data) => { onSuccess: (data) => {
console.log("admin page"); setAllFlows([]);
setSelectedFolder(null);
setLoading(true); setLoading(true);
login(data.access_token, "login"); login(data.access_token, "login");
navigate("/admin/"); navigate("/admin/");

View file

@ -26,9 +26,9 @@ const useAuthStore = create<AuthStoreType>((set, get) => ({
set({ authenticationErrorCount }), set({ authenticationErrorCount }),
logout: async () => { logout: async () => {
const setAllFlows = useFlowsManagerStore.getState().setAllFlows; get().setIsAuthenticated(false);
const setSelectedFolder = useFolderStore.getState().setSelectedFolder; get().setIsAdmin(false);
cookies.remove("apikey_tkn_lflw", { path: "/" });
set({ set({
isAdmin: false, isAdmin: false,
userData: null, userData: null,
@ -37,8 +37,8 @@ const useAuthStore = create<AuthStoreType>((set, get) => ({
autoLogin: false, autoLogin: false,
apiKey: null, apiKey: null,
}); });
setAllFlows([]);
setSelectedFolder(null); window.location.href = "/login";
}, },
// getUser: () => { // getUser: () => {
// const setLoading = useAlertStore.getState().setLoading; // const setLoading = useAlertStore.getState().setLoading;

View file

@ -3,7 +3,6 @@ import { Users } from "../api";
export type AuthContextType = { export type AuthContextType = {
accessToken: string | null; accessToken: string | null;
login: (accessToken: string, autoLogin: string) => void; login: (accessToken: string, autoLogin: string) => void;
logout: () => Promise<void>;
userData: Users | null; userData: Users | null;
setUserData: (userData: Users | null) => void; setUserData: (userData: Users | null) => void;
authenticationErrorCount: number; authenticationErrorCount: number;

View file

@ -32,6 +32,10 @@ export default defineConfig(({ mode }) => {
}, },
define: { define: {
"process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL), "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()], plugins: [react(), svgr(), tsconfigPaths()],
server: { server: {