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:
parent
a421a7dfcc
commit
7c75ad0728
12 changed files with 84 additions and 69 deletions
|
|
@ -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()]);
|
||||
|
|
|
|||
|
|
@ -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 <Navigate to="/" replace />;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<undefined, void> = (
|
|||
options?,
|
||||
) => {
|
||||
const { mutate } = UseRequestProcessor();
|
||||
const navigate = useNavigate();
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
|
||||
async function logoutUser(): Promise<any> {
|
||||
|
|
@ -29,7 +23,6 @@ export const useLogout: useMutationFunctionType<undefined, void> = (
|
|||
const mutation = mutate(["useLogout"], logoutUser, {
|
||||
onSuccess: () => {
|
||||
logout();
|
||||
navigate("/login");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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/");
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ const useAuthStore = create<AuthStoreType>((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<AuthStoreType>((set, get) => ({
|
|||
autoLogin: false,
|
||||
apiKey: null,
|
||||
});
|
||||
setAllFlows([]);
|
||||
setSelectedFolder(null);
|
||||
|
||||
window.location.href = "/login";
|
||||
},
|
||||
// getUser: () => {
|
||||
// const setLoading = useAlertStore.getState().setLoading;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Users } from "../api";
|
|||
export type AuthContextType = {
|
||||
accessToken: string | null;
|
||||
login: (accessToken: string, autoLogin: string) => void;
|
||||
logout: () => Promise<void>;
|
||||
userData: Users | null;
|
||||
setUserData: (userData: Users | null) => void;
|
||||
authenticationErrorCount: number;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue