@@ -51,30 +55,30 @@ export default function PaginatorComponent({
- Page {index} of {maxIndex}
+ Page {currentPage} of {maxIndex}
{
- setPageIndex(1);
- paginate(size, 1);
+ setPageIndex(0);
+ setCurrentPage(1);
+ paginate(size, 0);
}}
>
Go to first page
{
- if (index <= 1) {
- setPageIndex(1);
- paginate(size, 1);
- } else {
- {
- setPageIndex(index - 1);
- paginate(size, index - 1);
- }
+ if (index > 0) {
+ const pgIndex = size - index;
+ setCurrentPage(currentPage - 1);
+ setPageIndex(pgIndex);
+ paginate(size, pgIndex);
}
}}
variant="outline"
@@ -84,14 +88,12 @@ export default function PaginatorComponent({
{
- if (index >= maxIndex) {
- setPageIndex(maxIndex);
- paginate(size, maxIndex);
- } else {
- setPageIndex(index + 1);
- paginate(size, index + 1);
- }
+ const pgIndex = size + index;
+ setPageIndex(pgIndex);
+ setCurrentPage(currentPage + 1);
+ paginate(size, pgIndex);
}}
variant="outline"
className="h-8 w-8 p-0"
@@ -100,11 +102,13 @@ export default function PaginatorComponent({
{
- setPageIndex(maxIndex);
- paginate(size, maxIndex);
+ setPageIndex(maxIndex - 1);
+ setCurrentPage(maxIndex);
+ paginate(size, size);
}}
>
Go to last page
diff --git a/src/frontend/src/components/authAdminGuard/index.tsx b/src/frontend/src/components/authAdminGuard/index.tsx
new file mode 100644
index 000000000..724d39f5a
--- /dev/null
+++ b/src/frontend/src/components/authAdminGuard/index.tsx
@@ -0,0 +1,30 @@
+import { useContext, useEffect } from "react";
+import { Navigate } from "react-router-dom";
+import { AuthContext } from "../../contexts/authContext";
+
+export const ProtectedAdminRoute = ({ children }) => {
+ const {
+ isAdmin,
+ isAuthenticated,
+ logout,
+ getAuthentication,
+ userData,
+ autoLogin,
+ } = useContext(AuthContext);
+ useEffect(() => {
+ if (!isAuthenticated && !getAuthentication()) {
+ window.location.replace("/login");
+ logout();
+ }
+ }, [isAuthenticated, getAuthentication, logout, userData]);
+
+ if (!isAuthenticated && !getAuthentication()) {
+ return ;
+ }
+
+ if ((userData && !isAdmin) || autoLogin) {
+ return ;
+ }
+
+ return children;
+};
diff --git a/src/frontend/src/components/authGuard/index.tsx b/src/frontend/src/components/authGuard/index.tsx
new file mode 100644
index 000000000..9b21a1c9f
--- /dev/null
+++ b/src/frontend/src/components/authGuard/index.tsx
@@ -0,0 +1,14 @@
+import { useContext } from "react";
+import { Navigate } from "react-router-dom";
+import { AuthContext } from "../../contexts/authContext";
+
+export const ProtectedRoute = ({ children }) => {
+ const { isAuthenticated, logout, getAuthentication } =
+ useContext(AuthContext);
+ if (!isAuthenticated && !getAuthentication()) {
+ logout();
+ return ;
+ }
+
+ return children;
+};
diff --git a/src/frontend/src/components/authLoginGuard/index.tsx b/src/frontend/src/components/authLoginGuard/index.tsx
new file mode 100644
index 000000000..980528023
--- /dev/null
+++ b/src/frontend/src/components/authLoginGuard/index.tsx
@@ -0,0 +1,19 @@
+import { useContext } from "react";
+import { Navigate } from "react-router-dom";
+import { AuthContext } from "../../contexts/authContext";
+
+export const ProtectedLoginRoute = ({ children }) => {
+ const { getAuthentication, autoLogin } = useContext(AuthContext);
+
+ if (autoLogin === true) {
+ window.location.replace("/");
+ return ;
+ }
+
+ if (getAuthentication()) {
+ window.location.replace("/");
+ return ;
+ }
+
+ return children;
+};
diff --git a/src/frontend/src/components/catchAllRoutes/index.tsx b/src/frontend/src/components/catchAllRoutes/index.tsx
new file mode 100644
index 000000000..06faa9099
--- /dev/null
+++ b/src/frontend/src/components/catchAllRoutes/index.tsx
@@ -0,0 +1,13 @@
+import { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+
+export const CatchAllRoute = () => {
+ const navigate = useNavigate();
+
+ // Redirect to the root ("/") when the catch-all route is matched
+ useEffect(() => {
+ navigate("/");
+ }, []);
+
+ return null;
+};
diff --git a/src/frontend/src/components/chatComponent/buildTrigger/index.tsx b/src/frontend/src/components/chatComponent/buildTrigger/index.tsx
index 0967952b5..66bc0d371 100644
--- a/src/frontend/src/components/chatComponent/buildTrigger/index.tsx
+++ b/src/frontend/src/components/chatComponent/buildTrigger/index.tsx
@@ -60,6 +60,11 @@ export default function BuildTrigger({
],
});
}
+ if (errors.length === 0 && allNodesValid) {
+ setSuccessData({
+ title: "Flow is ready to run",
+ });
+ }
} catch (error) {
console.error("Error:", error);
} finally {
diff --git a/src/frontend/src/components/codeTabsComponent/index.tsx b/src/frontend/src/components/codeTabsComponent/index.tsx
index e60e09ae5..fcd7385d7 100644
--- a/src/frontend/src/components/codeTabsComponent/index.tsx
+++ b/src/frontend/src/components/codeTabsComponent/index.tsx
@@ -28,8 +28,11 @@ import {
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
+import { alertContext } from "../../contexts/alertContext";
import { darkContext } from "../../contexts/darkContext";
+import { typesContext } from "../../contexts/typesContext";
import { codeTabsPropsType } from "../../types/components";
+import { unselectAllNodes } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
@@ -45,6 +48,8 @@ export default function CodeTabsComponent({
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
const [openAccordion, setOpenAccordion] = useState([]);
const { dark } = useContext(darkContext);
+ const { reactFlowInstance } = useContext(typesContext);
+ const { isTweakPage, setIsTweakPage } = useContext(alertContext);
useEffect(() => {
if (flow && flow["data"]!["nodes"]) {
@@ -52,6 +57,19 @@ export default function CodeTabsComponent({
}
}, [flow]);
+ useEffect(() => {
+ unselectAllNodes({
+ data,
+ updateNodes: (nodes) => {
+ reactFlowInstance?.setNodes(nodes);
+ },
+ });
+
+ return () => {
+ if (isTweakPage) setIsTweakPage(false);
+ };
+ }, []);
+
const copyToClipboard = () => {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return;
@@ -159,13 +177,13 @@ export default function CodeTabsComponent({
)}
- {tabs.map((tab, index) => (
+ {tabs.map((tab, idx) => (
- {index < 4 ? (
+ {idx < 4 ? (
<>
{tab.description && (
>
- ) : index === 4 ? (
+ ) : idx === 4 ? (
<>
- {data?.map((node: any, index) => (
-
+ {data?.map((node: any, i) => (
+
{tweaks?.tweaksList!.current.includes(
node["data"]["id"]
) && (
@@ -236,10 +254,10 @@ export default function CodeTabsComponent({
node.data.node.template[templateField]
.type === "int")
)
- .map((templateField, index) => {
+ .map((templateField, indx) => {
return (
@@ -278,7 +296,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = target;
@@ -327,7 +345,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = target;
@@ -372,7 +390,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = target;
@@ -405,7 +423,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = e;
@@ -496,7 +514,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = target;
@@ -532,7 +550,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = target;
@@ -584,7 +602,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = target;
@@ -639,7 +657,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = target;
@@ -694,7 +712,7 @@ export default function CodeTabsComponent({
let newInputList =
cloneDeep(old);
newInputList![
- index
+ i
].data.node.template[
templateField
].value = target;
diff --git a/src/frontend/src/components/fetchErrorComponent/index.tsx b/src/frontend/src/components/fetchErrorComponent/index.tsx
new file mode 100644
index 000000000..6004d9dfc
--- /dev/null
+++ b/src/frontend/src/components/fetchErrorComponent/index.tsx
@@ -0,0 +1,16 @@
+import { fetchErrorComponentType } from "../../types/components";
+import IconComponent from "../genericIconComponent";
+
+export default function FetchErrorComponent({
+ message,
+ description,
+}: fetchErrorComponentType) {
+ return (
+
+
+
+ {message}
+ {description}
+
+ );
+}
diff --git a/src/frontend/src/components/genericIconComponent/index.tsx b/src/frontend/src/components/genericIconComponent/index.tsx
index 186c2e253..5cb6bdeca 100644
--- a/src/frontend/src/components/genericIconComponent/index.tsx
+++ b/src/frontend/src/components/genericIconComponent/index.tsx
@@ -1,17 +1,19 @@
+import { forwardRef } from "react";
import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
-export default function IconComponent({
- name,
- className,
- iconColor,
-}: IconComponentProps): JSX.Element {
- const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
- return (
-
- );
-}
+const ForwardedIconComponent = forwardRef(
+ ({ name, className, iconColor }: IconComponentProps, ref) => {
+ const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
+ return (
+
+ );
+ }
+);
+
+export default ForwardedIconComponent;
diff --git a/src/frontend/src/components/headerComponent/index.tsx b/src/frontend/src/components/headerComponent/index.tsx
index e9a5a36a9..c74cdaa76 100644
--- a/src/frontend/src/components/headerComponent/index.tsx
+++ b/src/frontend/src/components/headerComponent/index.tsx
@@ -1,12 +1,12 @@
-import { useContext, useEffect, useState } from "react";
+import { useContext } from "react";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
-import { Link, useLocation } from "react-router-dom";
+import { Link, useLocation, useNavigate } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
+import { AuthContext } from "../../contexts/authContext";
import { darkContext } from "../../contexts/darkContext";
import { TabsContext } from "../../contexts/tabsContext";
-import { getRepoStars } from "../../controllers/API";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
@@ -17,29 +17,54 @@ export default function Header(): JSX.Element {
const { dark, setDark } = useContext(darkContext);
const { notificationCenter } = useContext(alertContext);
const location = useLocation();
+ const { logout, autoLogin, isAdmin } = useContext(AuthContext);
+ const { stars } = useContext(darkContext);
+ const navigate = useNavigate();
- const [stars, setStars] = useState(null);
-
- // Get and set numbers of stars on header
- useEffect(() => {
- async function fetchStars() {
- const starsCount = await getRepoStars("logspace-ai", "langflow");
- setStars(starsCount);
- }
- fetchStars();
- }, []);
return (
@@ -119,6 +144,18 @@ export default function Header(): JSX.Element {
/>
+ {!autoLogin && (
+
{
+ navigate("/account/api-keys");
+ }}
+ >
+
+
+ )}
diff --git a/src/frontend/src/components/inputComponent/index.tsx b/src/frontend/src/components/inputComponent/index.tsx
index 287ceac63..987c53a8f 100644
--- a/src/frontend/src/components/inputComponent/index.tsx
+++ b/src/frontend/src/components/inputComponent/index.tsx
@@ -81,7 +81,8 @@ export default function InputComponent({
? "input-component-true-button"
: "input-component-false-button"
)}
- onClick={() => {
+ onClick={(event) => {
+ event.preventDefault();
setPwdVisible(!pwdVisible);
}}
>
diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts
index 7f6c7614c..8488ca890 100644
--- a/src/frontend/src/constants/constants.ts
+++ b/src/frontend/src/constants/constants.ts
@@ -508,6 +508,7 @@ export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"/api/v1/validate/code",
"/api/v1/custom_component",
"/api/v1/validate/prompt",
+ "http://localhost:7860/login",
];
export const skipNodeUpdate = ["CustomComponent"];
@@ -522,6 +523,18 @@ export const CONTROL_LOGIN_STATE = {
username: "",
password: "",
};
+
+export const CONTROL_NEW_USER = {
+ username: "",
+ password: "",
+ is_active: false,
+ is_superuser: false,
+};
+
+export const CONTROL_NEW_API_KEY = {
+ apikeyname: "",
+};
+
export const tabsCode = [];
export function tabsArray(codes: string[], method: number) {
@@ -602,3 +615,24 @@ export function tabsArray(codes: string[], method: number) {
},
];
}
+export const FETCH_ERROR_MESSAGE = "Couldn't establish a connection.";
+export const FETCH_ERROR_DESCRIPION =
+ "Check if everything is working properly and try again.";
+
+export const BASE_URL_API = "/api/v1/";
+
+export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
+
+export const API_PAGE_PARAGRAPH_1 =
+ "Your secret API keys are listed below. Please note that we do not display your secret API keys again after you generate them.";
+
+export const API_PAGE_PARAGRAPH_2 =
+ "Do not share your API key with others, or expose it in the browser or other client-side code.";
+
+export const API_PAGE_USER_KEYS =
+ "This user does not have any keys assigned at the moment.";
+
+export const LAST_USED_SPAN_1 = "The last time this key was used.";
+
+export const LAST_USED_SPAN_2 =
+ "Accurate to within the hour from the most recent usage.";
diff --git a/src/frontend/src/contexts/alertContext.tsx b/src/frontend/src/contexts/alertContext.tsx
index 1741b88b6..98b2fdef8 100644
--- a/src/frontend/src/contexts/alertContext.tsx
+++ b/src/frontend/src/contexts/alertContext.tsx
@@ -26,6 +26,8 @@ const initialValue: alertContextType = {
pushNotificationList: () => {},
clearNotificationList: () => {},
removeFromNotificationList: () => {},
+ isTweakPage: false,
+ setIsTweakPage: () => {},
};
export const alertContext = createContext
(initialValue);
@@ -48,6 +50,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
const [successOpen, setSuccessOpen] = useState(false);
const [notificationCenter, setNotificationCenter] = useState(false);
const [notificationList, setNotificationList] = useState([]);
+ const [isTweakPage, setIsTweakPage] = useState(false);
const pushNotificationList = (notification: AlertItemType) => {
setNotificationList((old) => {
let newNotificationList = _.cloneDeep(old);
@@ -120,6 +123,8 @@ export function AlertProvider({ children }: { children: ReactNode }) {
return (
false,
isAuthenticated: false,
accessToken: null,
+ refreshToken: null,
login: () => {},
logout: () => {},
- refreshAccessToken: () => Promise.resolve(),
userData: null,
setUserData: () => {},
+ getAuthentication: () => false,
+ authenticationErrorCount: 0,
+ autoLogin: false,
+ setAutoLogin: () => {},
};
-const AuthContext = createContext(initialValue);
+export const AuthContext = createContext(initialValue);
export function AuthProvider({ children }): React.ReactElement {
- const [accessToken, setAccessToken] = useState(null);
- const [userData, setUserData] = useState(null);
-
+ const cookies = new Cookies();
+ const [accessToken, setAccessToken] = useState(
+ cookies.get("access_token")
+ );
+ const [refreshToken, setRefreshToken] = useState(
+ cookies.get("refresh_token")
+ );
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [userData, setUserData] = useState(null);
+ const [autoLogin, setAutoLogin] = useState(false);
+ const { setLoading } = useContext(alertContext);
useEffect(() => {
- const storedAccessToken = localStorage.getItem("access_token");
+ const storedAccessToken = cookies.get("access_token");
if (storedAccessToken) {
setAccessToken(storedAccessToken);
}
}, []);
+ useEffect(() => {
+ const isLoginPage = location.pathname.includes("login");
+
+ autoLoginApi()
+ .then((user) => {
+ if (user && user["access_token"]) {
+ user["refresh_token"] = "auto";
+ login(user["access_token"], user["refresh_token"]);
+ setUserData(user);
+ setAutoLogin(true);
+ setLoading(false);
+ }
+ })
+ .catch((error) => {
+ setAutoLogin(false);
+ if (getAuthentication() && !isLoginPage) {
+ getLoggedUser()
+ .then((user) => {
+ setUserData(user);
+ setLoading(false);
+ const isSuperUser = user.is_superuser;
+ setIsAdmin(isSuperUser);
+ })
+ .catch((error) => {});
+ } else {
+ setLoading(false);
+ }
+ });
+ }, []);
+
+ function getAuthentication() {
+ const storedRefreshToken = cookies.get("refresh_token");
+ const storedAccess = cookies.get("access_token");
+ const auth = storedAccess && storedRefreshToken ? true : false;
+ return auth;
+ }
+
function login(newAccessToken: string, refreshToken: string) {
- localStorage.setItem("access_token", newAccessToken);
+ cookies.set("access_token", newAccessToken, { path: "/" });
+ cookies.set("refresh_token", refreshToken, { path: "/" });
setAccessToken(newAccessToken);
- // Store refreshToken if needed
+ setRefreshToken(refreshToken);
+ setIsAuthenticated(true);
}
function logout() {
- localStorage.removeItem("access_token");
- // Clear refreshToken if used
+ cookies.remove("access_token", { path: "/" });
+ cookies.remove("refresh_token", { path: "/" });
+ setIsAdmin(false);
+ setUserData(null);
setAccessToken(null);
- }
-
- async function refreshAccessToken(refreshToken: string) {
- try {
- // Call your API to refresh the access token using the refresh token
- const response = await fetch("/api/refresh-token", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ refreshToken }),
- });
-
- if (response.ok) {
- const data = await response.json();
- login(data.accessToken, refreshToken);
- } else {
- logout();
- }
- } catch (error) {
- logout();
- }
+ setRefreshToken(null);
+ setIsAuthenticated(false);
}
return (
// !! to convert string to boolean
{children}
diff --git a/src/frontend/src/contexts/darkContext.tsx b/src/frontend/src/contexts/darkContext.tsx
index 091b7577f..bfb758009 100644
--- a/src/frontend/src/contexts/darkContext.tsx
+++ b/src/frontend/src/contexts/darkContext.tsx
@@ -1,9 +1,12 @@
import { createContext, useEffect, useState } from "react";
+import { getRepoStars } from "../controllers/API";
import { darkContextType } from "../types/typesContext";
const initialValue = {
dark: {},
setDark: () => {},
+ stars: 0,
+ setStars: (stars) => 0,
};
export const darkContext = createContext(initialValue);
@@ -12,6 +15,16 @@ export function DarkProvider({ children }) {
const [dark, setDark] = useState(
JSON.parse(window.localStorage.getItem("isDark")!) ?? false
);
+ const [stars, setStars] = useState(0);
+
+ useEffect(() => {
+ async function fetchStars() {
+ const starsCount = await getRepoStars("logspace-ai", "langflow");
+ setStars(starsCount);
+ }
+ fetchStars();
+ }, []);
+
useEffect(() => {
if (dark) {
document.getElementById("body")!.classList.add("dark");
@@ -20,9 +33,12 @@ export function DarkProvider({ children }) {
}
window.localStorage.setItem("isDark", dark.toString());
}, [dark]);
+
return (
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx
index 405a76a1e..9b118afb1 100644
--- a/src/frontend/src/contexts/tabsContext.tsx
+++ b/src/frontend/src/contexts/tabsContext.tsx
@@ -1,3 +1,4 @@
+import { AxiosError } from "axios";
import _ from "lodash";
import {
ReactNode,
@@ -27,7 +28,7 @@ import {
sourceHandleType,
targetHandleType,
} from "../types/flow";
-import { TabsContextType, TabsState, errorsVarType } from "../types/tabs";
+import { TabsContextType, TabsState } from "../types/tabs";
import {
addVersionToDuplicates,
checkOldEdgesHandles,
@@ -39,6 +40,7 @@ import {
} from "../utils/reactflowUtils";
import { getRandomDescription, getRandomName } from "../utils/utils";
import { alertContext } from "./alertContext";
+import { AuthContext } from "./authContext";
import { typesContext } from "./typesContext";
const uid = new ShortUniqueId({ length: 5 });
@@ -78,7 +80,9 @@ export const TabsContext = createContext(
);
export function TabsProvider({ children }: { children: ReactNode }) {
- const { setErrorData, setNoticeData } = useContext(alertContext);
+ const { setErrorData, setNoticeData, setSuccessData } =
+ useContext(alertContext);
+ const { getAuthentication } = useContext(AuthContext);
const [tabId, setTabId] = useState("");
@@ -127,24 +131,26 @@ export function TabsProvider({ children }: { children: ReactNode }) {
try {
processDBData(DbData);
updateStateWithDbData(DbData);
- } catch (e) {
- console.error(e);
- }
+ } catch (e) {}
}
});
}
useEffect(() => {
- // get data from db
- //get tabs locally saved
- // let tabsData = getLocalStorageTabsData();
- refreshFlows();
- }, [templates]);
+ // If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks.
+ if (getAuthentication() === true) {
+ // get data from db
+ //get tabs locally saved
+ // let tabsData = getLocalStorageTabsData();
+ refreshFlows();
+ }
+ }, [templates, getAuthentication()]);
function getTabsDataFromDB() {
//get tabs from db
return readFlowsFromDatabase();
}
+
function processDBData(DbData: FlowType[]) {
DbData.forEach((flow: FlowType) => {
try {
@@ -153,9 +159,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
processFlowEdges(flow);
processFlowNodes(flow);
- } catch (e) {
- console.error(e);
- }
+ } catch (e) {}
});
}
@@ -497,7 +501,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return id;
} catch (error) {
// Handle the error if needed
- console.error("Error while adding flow:", error);
throw error; // Re-throw the error so the caller can handle it if needed
}
} else {
@@ -603,6 +606,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const updatedFlow = await updateFlowInDatabase(newFlow);
if (updatedFlow) {
// updates flow in state
+ setSuccessData({ title: "Changes saved successfully" });
setFlows((prevState) => {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
@@ -625,7 +629,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
}
} catch (err) {
- setErrorData(err as errorsVarType);
+ setErrorData({
+ title: "Error while saving changes",
+ list: [(err as AxiosError).message],
+ });
}
}
diff --git a/src/frontend/src/contexts/typesContext.tsx b/src/frontend/src/contexts/typesContext.tsx
index 52a5eea72..5521e8ebf 100644
--- a/src/frontend/src/contexts/typesContext.tsx
+++ b/src/frontend/src/contexts/typesContext.tsx
@@ -6,10 +6,11 @@ import {
useState,
} from "react";
import { Node, ReactFlowInstance } from "reactflow";
-import { getAll } from "../controllers/API";
+import { getAll, getHealth } from "../controllers/API";
import { APIKindType } from "../types/api";
import { typesContextType } from "../types/typesContext";
import { alertContext } from "./alertContext";
+import { AuthContext } from "./authContext";
//context to share types adn functions from nodes to flow
@@ -23,6 +24,8 @@ const initialValue: typesContextType = {
setTemplates: () => {},
data: {},
setData: () => {},
+ setFetchError: () => {},
+ fetchError: false,
};
export const typesContext = createContext(initialValue);
@@ -33,67 +36,58 @@ export function TypesProvider({ children }: { children: ReactNode }) {
useState(null);
const [templates, setTemplates] = useState({});
const [data, setData] = useState({});
+ const [fetchError, setFetchError] = useState(false);
const { setLoading } = useContext(alertContext);
+ const { getAuthentication } = useContext(AuthContext);
useEffect(() => {
- let delay = 1000; // Start delay of 1 second
- let intervalId: NodeJS.Timer;
- let retryCount = 0; // Count of retry attempts
- const maxRetryCount = 5; // Max retry attempts
+ // If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks.
+ if (getAuthentication() === true) {
+ getTypes();
+ }
+ }, [getAuthentication()]);
+ async function getTypes(): Promise {
// We will keep a flag to handle the case where the component is unmounted before the API call resolves.
let isMounted = true;
-
- async function getTypes(): Promise {
- try {
- const result = await getAll();
- // Make sure to only update the state if the component is still mounted.
- if (isMounted) {
- setLoading(false);
- setData(result.data);
- setTemplates(
- Object.keys(result.data).reduce((acc, curr) => {
+ try {
+ const result = await getAll();
+ // Make sure to only update the state if the component is still mounted.
+ if (isMounted && result?.status === 200) {
+ setLoading(false);
+ setData(result.data);
+ setTemplates(
+ Object.keys(result.data).reduce((acc, curr) => {
+ Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
+ acc[c] = result.data[curr][c];
+ });
+ return acc;
+ }, {})
+ );
+ // Set the types by reducing over the keys of the result data and updating the accumulator.
+ setTypes(
+ // Reverse the keys so the tool world does not overlap
+ Object.keys(result.data)
+ .reverse()
+ .reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
- acc[c] = result.data[curr][c];
+ acc[c] = curr;
+ // Add the base classes to the accumulator as well.
+ result.data[curr][c].base_classes?.forEach((b) => {
+ acc[b] = curr;
+ });
});
return acc;
}, {})
- );
- // Set the types by reducing over the keys of the result data and updating the accumulator.
- setTypes(
- // Reverse the keys so the tool world does not overlap
- Object.keys(result.data)
- .reverse()
- .reduce((acc, curr) => {
- Object.keys(result.data[curr]).forEach(
- (c: keyof APIKindType) => {
- acc[c] = curr;
- // Add the base classes to the accumulator as well.
- result.data[curr][c].base_classes?.forEach((b) => {
- acc[b] = curr;
- });
- }
- );
- return acc;
- }, {})
- );
- }
- // Clear the interval if successful.
- clearInterval(intervalId!);
- } catch (error) {
- console.error("An error has occurred while fetching types.");
+ );
}
+ } catch (error) {
+ console.error("An error has occurred while fetching types.");
+ await getHealth().catch((e) => {
+ setFetchError(true);
+ });
}
-
- // Start the initial interval.
- intervalId = setInterval(getTypes, delay);
- return () => {
- // This will clear the interval when the component unmounts, or when the dependencies of the useEffect hook change.
- clearInterval(intervalId!);
- // Indicate that the component has been unmounted.
- isMounted = false;
- };
- }, []);
+ }
function deleteNode(idx: string) {
reactFlowInstance!.setNodes(
@@ -117,6 +111,8 @@ export function TypesProvider({ children }: { children: ReactNode }) {
templates,
data,
setData,
+ fetchError,
+ setFetchError,
}}
>
{children}
diff --git a/src/frontend/src/controllers/API/api.tsx b/src/frontend/src/controllers/API/api.tsx
index ec0e8f329..59fba4393 100644
--- a/src/frontend/src/controllers/API/api.tsx
+++ b/src/frontend/src/controllers/API/api.tsx
@@ -1,60 +1,117 @@
import axios, { AxiosError, AxiosInstance } from "axios";
-import { useContext, useEffect, useRef } from "react";
+import { useContext, useEffect } from "react";
+import { Cookies } from "react-cookie";
+import { useNavigate } from "react-router-dom";
+import { renewAccessToken } from ".";
import { alertContext } from "../../contexts/alertContext";
+import { AuthContext } from "../../contexts/authContext";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
baseURL: "",
});
-function ApiInterceptor(): null {
- const retryCounts = useRef([]);
+function ApiInterceptor() {
const { setErrorData } = useContext(alertContext);
+ let { accessToken, login, logout, authenticationErrorCount } =
+ useContext(AuthContext);
+ const navigate = useNavigate();
+ const cookies = new Cookies();
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
- // if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
- // return Promise.reject(error);
- // }
- // let retryCount = 0;
- // while (retryCount < 4) {
- // await sleep(5000); // Sleep for 5 seconds
- // retryCount++;
- // try {
- // const response = await axios.request(error.config);
- // return response;
- // } catch (error) {
- // if (retryCount === 3) {
- // setErrorData({
- // title: "There was an error on web connection, please: ",
- // list: [
- // "Refresh the page",
- // "Use a new flow tab",
- // "Check if the backend is up",
- // "Endpoint: " + error.config?.url,
- // ],
- // });
- // return Promise.reject(error);
- // }
- // }
- // }
+ if (error.response?.status === 401) {
+ const refreshToken = cookies.get("refresh_token");
+ if (refreshToken && refreshToken !== "auto") {
+ authenticationErrorCount = authenticationErrorCount + 1;
+ if (authenticationErrorCount > 3) {
+ authenticationErrorCount = 0;
+ logout();
+ navigate("/login");
+ }
+
+ const res = await renewAccessToken(refreshToken);
+ login(res.data.access_token, res.data.refresh_token);
+ try {
+ if (error?.config?.headers) {
+ delete error.config.headers["Authorization"];
+ error.config.headers["Authorization"] = `Bearer ${accessToken}`;
+ const response = await axios.request(error.config);
+ return response;
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.status === 401) {
+ logout();
+ navigate("/login");
+ }
+ }
+ }
+
+ if (!refreshToken && error?.config?.url?.includes("login")) {
+ return Promise.reject(error);
+ } else {
+ logout();
+ navigate("/login");
+ }
+ } else {
+ // if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
+ return Promise.reject(error);
+ // }
+ }
+ }
+ );
+
+ const isAuthorizedURL = (url) => {
+ const authorizedDomains = [
+ "https://raw.githubusercontent.com/logspace-ai/langflow_examples/main/examples",
+ "https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples",
+ "https://api.github.com/repos/logspace-ai/langflow",
+ "auto_login",
+ ];
+
+ const authorizedEndpoints = ["auto_login"];
+
+ try {
+ const parsedURL = new URL(url);
+
+ const isDomainAllowed = authorizedDomains.some(
+ (domain) => parsedURL.origin === new URL(domain).origin
+ );
+ const isEndpointAllowed = authorizedEndpoints.some((endpoint) =>
+ parsedURL.pathname.includes(endpoint)
+ );
+
+ return isDomainAllowed || isEndpointAllowed;
+ } catch (e) {
+ // Invalid URL
+ return false;
+ }
+ };
+
+ // Request interceptor to add access token to every request
+ const requestInterceptor = api.interceptors.request.use(
+ (config) => {
+ if (accessToken && !isAuthorizedURL(config?.url)) {
+ config.headers["Authorization"] = `Bearer ${accessToken}`;
+ }
+
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
}
);
return () => {
- // Clean up the interceptor when the component unmounts
+ // Clean up the interceptors when the component unmounts
api.interceptors.response.eject(interceptor);
+ api.interceptors.request.eject(requestInterceptor);
};
- }, [retryCounts]);
+ }, [accessToken, setErrorData]);
return null;
}
-// Function to sleep for a given duration in milliseconds
-function sleep(ms: number) {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
export { ApiInterceptor, api };
diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts
index be3d4f2e9..3e6c78801 100644
--- a/src/frontend/src/controllers/API/index.ts
+++ b/src/frontend/src/controllers/API/index.ts
@@ -1,7 +1,14 @@
import { AxiosResponse } from "axios";
import { ReactFlowJsonObject } from "reactflow";
+import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
-import { APIObjectType, sendAllProps } from "../../types/api/index";
+import {
+ APIObjectType,
+ LoginType,
+ Users,
+ sendAllProps,
+} from "../../types/api/index";
+import { UserInputType } from "../../types/components";
import { FlowStyleType, FlowType } from "../../types/flow";
import {
APIClassType,
@@ -18,7 +25,7 @@ import {
* @returns {Promise>} A promise that resolves to an AxiosResponse containing all the objects.
*/
export async function getAll(): Promise> {
- return await api.get(`/api/v1/all`);
+ return await api.get(`${BASE_URL_API}all`);
}
const GITHUB_API_URL = "https://api.github.com";
@@ -40,13 +47,13 @@ export async function getRepoStars(owner: string, repo: string) {
* @returns {AxiosResponse} The API response.
*/
export async function sendAll(data: sendAllProps) {
- return await api.post(`/api/v1/predict`, data);
+ return await api.post(`${BASE_URL_API}predict`, data);
}
export async function postValidateCode(
code: string
): Promise> {
- return await api.post("/api/v1/validate/code", { code });
+ return await api.post(`${BASE_URL_API}validate/code`, { code });
}
/**
@@ -61,7 +68,7 @@ export async function postValidatePrompt(
template: string,
frontend_node: APIClassType
): Promise> {
- return await api.post("/api/v1/validate/prompt", {
+ return await api.post(`${BASE_URL_API}validate/prompt`, {
name: name,
template: template,
frontend_node: frontend_node,
@@ -105,7 +112,7 @@ export async function saveFlowToDatabase(newFlow: {
style?: FlowStyleType;
}): Promise {
try {
- const response = await api.post("/api/v1/flows/", {
+ const response = await api.post(`${BASE_URL_API}flows/`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
@@ -131,7 +138,7 @@ export async function updateFlowInDatabase(
updatedFlow: FlowType
): Promise {
try {
- const response = await api.patch(`/api/v1/flows/${updatedFlow.id}`, {
+ const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
name: updatedFlow.name,
data: updatedFlow.data,
description: updatedFlow.description,
@@ -155,8 +162,8 @@ export async function updateFlowInDatabase(
*/
export async function readFlowsFromDatabase() {
try {
- const response = await api.get("/api/v1/flows/");
- if (response.status !== 200) {
+ const response = await api.get(`${BASE_URL_API}flows/`);
+ if (response?.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
@@ -168,8 +175,8 @@ export async function readFlowsFromDatabase() {
export async function downloadFlowsFromDatabase() {
try {
- const response = await api.get("/api/v1/flows/download/");
- if (response.status !== 200) {
+ const response = await api.get(`${BASE_URL_API}flows/download/`);
+ if (response?.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
@@ -181,7 +188,7 @@ export async function downloadFlowsFromDatabase() {
export async function uploadFlowsToDatabase(flows: FormData) {
try {
- const response = await api.post(`/api/v1/flows/upload/`, flows);
+ const response = await api.post(`${BASE_URL_API}flows/upload/`, flows);
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -202,7 +209,7 @@ export async function uploadFlowsToDatabase(flows: FormData) {
*/
export async function deleteFlowFromDatabase(flowId: string) {
try {
- const response = await api.delete(`/api/v1/flows/${flowId}`);
+ const response = await api.delete(`${BASE_URL_API}flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -222,7 +229,7 @@ export async function deleteFlowFromDatabase(flowId: string) {
*/
export async function getFlowFromDatabase(flowId: number) {
try {
- const response = await api.get(`/api/v1/flows/${flowId}`);
+ const response = await api.get(`${BASE_URL_API}flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -241,7 +248,7 @@ export async function getFlowFromDatabase(flowId: number) {
*/
export async function getFlowStylesFromDatabase() {
try {
- const response = await api.get("/api/v1/flow_styles/");
+ const response = await api.get(`${BASE_URL_API}flow_styles/`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -261,7 +268,7 @@ export async function getFlowStylesFromDatabase() {
*/
export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
try {
- const response = await api.post("/api/v1/flow_styles/", flowStyle, {
+ const response = await api.post(`${BASE_URL_API}flow_styles/`, flowStyle, {
headers: {
accept: "application/json",
"Content-Type": "application/json",
@@ -284,7 +291,7 @@ export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
* @returns {Promise>} A promise that resolves to an AxiosResponse containing the version information.
*/
export async function getVersion() {
- const respnose = await api.get("/api/v1/version");
+ const respnose = await api.get(`${BASE_URL_API}version`);
return respnose.data;
}
@@ -306,7 +313,7 @@ export async function getHealth() {
export async function getBuildStatus(
flowId: string
): Promise {
- return await api.get(`/api/v1/build/${flowId}/status`);
+ return await api.get(`${BASE_URL_API}build/${flowId}/status`);
}
//docs for postbuildinit
@@ -319,7 +326,7 @@ export async function getBuildStatus(
export async function postBuildInit(
flow: FlowType
): Promise> {
- return await api.post(`/api/v1/build/init/${flow.id}`, flow);
+ return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
}
// fetch(`/upload/${id}`, {
@@ -337,12 +344,160 @@ export async function uploadFile(
): Promise> {
const formData = new FormData();
formData.append("file", file);
- return await api.post(`/api/v1/upload/${id}`, formData);
+ return await api.post(`${BASE_URL_API}upload/${id}`, formData);
}
export async function postCustomComponent(
code: string,
apiClass: APIClassType
): Promise> {
- return await api.post(`/api/v1/custom_component`, { code });
+ return await api.post(`${BASE_URL_API}custom_component`, { code });
+}
+
+export async function onLogin(user: LoginType) {
+ try {
+ const response = await api.post(
+ `${BASE_URL_API}login`,
+ new URLSearchParams({
+ username: user.username,
+ password: user.password,
+ }).toString(),
+ {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ }
+ );
+
+ if (response.status === 200) {
+ const data = response.data;
+ return data;
+ }
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function autoLogin() {
+ try {
+ const response = await api.get(`${BASE_URL_API}auto_login`);
+
+ if (response.status === 200) {
+ const data = response.data;
+ return data;
+ }
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function renewAccessToken(token: string) {
+ try {
+ return await api.post(`${BASE_URL_API}refresh?token=${token}`);
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
+}
+
+export async function getLoggedUser(): Promise {
+ try {
+ const res = await api.get(`${BASE_URL_API}user`);
+
+ if (res.status === 200) {
+ return res.data;
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
+}
+
+export async function addUser(user: UserInputType): Promise {
+ try {
+ const res = await api.post(`${BASE_URL_API}user`, user);
+ if (res.status === 200) {
+ return res.data;
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
+}
+
+export async function getUsersPage(
+ skip: number,
+ limit: number
+): Promise<[Users]> {
+ try {
+ const res = await api.get(
+ `${BASE_URL_API}users?skip=${skip}&limit=${limit}`
+ );
+ if (res.status === 200) {
+ return res.data;
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
+}
+
+export async function deleteUser(user_id: string) {
+ try {
+ const res = await api.delete(`${BASE_URL_API}user/${user_id}`);
+ if (res.status === 200) {
+ return res.data;
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
+}
+
+export async function updateUser(user_id: string, user: Users) {
+ try {
+ const res = await api.patch(`${BASE_URL_API}user/${user_id}`, user);
+ if (res.status === 200) {
+ return res.data;
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
+}
+
+export async function getApiKey() {
+ try {
+ const res = await api.get(`${BASE_URL_API}api_key`);
+ if (res.status === 200) {
+ return res.data;
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
+}
+
+export async function createApiKey(name: string) {
+ try {
+ const res = await api.post(`${BASE_URL_API}api_key`, { name });
+ if (res.status === 200) {
+ return res.data;
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
+}
+
+export async function deleteApiKey(api_key: string) {
+ try {
+ const res = await api.delete(`${BASE_URL_API}api_key/${api_key}`);
+ if (res.status === 200) {
+ return res.data;
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ throw error;
+ }
}
diff --git a/src/frontend/src/index.tsx b/src/frontend/src/index.tsx
index 2542f4903..78a57298f 100644
--- a/src/frontend/src/index.tsx
+++ b/src/frontend/src/index.tsx
@@ -1,10 +1,8 @@
import ReactDOM from "react-dom/client";
-import { BrowserRouter } from "react-router-dom";
import App from "./App";
import ContextWrapper from "./contexts";
import reportWebVitals from "./reportWebVitals";
-import { ApiInterceptor } from "./controllers/API/api";
// @ts-ignore
import "./style/index.css";
// @ts-ignore
@@ -17,10 +15,7 @@ const root = ReactDOM.createRoot(
);
root.render(
-
-
-
-
+
);
reportWebVitals();
diff --git a/src/frontend/src/modals/SecretKeyModal/index.tsx b/src/frontend/src/modals/SecretKeyModal/index.tsx
new file mode 100644
index 000000000..0487d28b8
--- /dev/null
+++ b/src/frontend/src/modals/SecretKeyModal/index.tsx
@@ -0,0 +1,202 @@
+import * as Form from "@radix-ui/react-form";
+import { useContext, useEffect, useRef, useState } from "react";
+import IconComponent from "../../components/genericIconComponent";
+import { Button } from "../../components/ui/button";
+import { Input } from "../../components/ui/input";
+import { CONTROL_NEW_API_KEY } from "../../constants/constants";
+import { alertContext } from "../../contexts/alertContext";
+import { createApiKey } from "../../controllers/API";
+import {
+ ApiKeyInputType,
+ ApiKeyType,
+ inputHandlerEventType,
+} from "../../types/components";
+import { nodeIconsLucide } from "../../utils/styleUtils";
+import BaseModal from "../baseModal";
+
+export default function SecretKeyModal({
+ title,
+ cancelText,
+ confirmationText,
+ children,
+ icon,
+ data,
+ onCloseModal,
+}: ApiKeyType) {
+ const Icon: any = nodeIconsLucide[icon];
+ const [open, setOpen] = useState(false);
+ const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
+ const [apiKeyValue, setApiKeyValue] = useState("");
+ const [inputState, setInputState] =
+ useState(CONTROL_NEW_API_KEY);
+ const [renderKey, setRenderKey] = useState(false);
+ const [textCopied, setTextCopied] = useState(true);
+ const { setSuccessData } = useContext(alertContext);
+ const inputRef = useRef(null);
+
+ function handleInput({
+ target: { name, value },
+ }: inputHandlerEventType): void {
+ setInputState((prev) => ({ ...prev, [name]: value }));
+ }
+
+ useEffect(() => {
+ if (open) {
+ setRenderKey(false);
+ resetForm();
+ } else {
+ onCloseModal();
+ }
+ }, [open]);
+
+ function resetForm() {
+ setApiKeyName("");
+ setApiKeyValue("");
+ }
+
+ const handleCopyClick = async () => {
+ if (apiKeyValue) {
+ await navigator.clipboard.writeText(apiKeyValue);
+ inputRef?.current?.focus();
+ inputRef?.current?.select();
+ setSuccessData({
+ title: "API Key copied!",
+ });
+ setTextCopied(false);
+
+ setTimeout(() => {
+ setTextCopied(true);
+ }, 3000);
+ }
+ };
+
+ function handleAddNewKey() {
+ createApiKey(apiKeyName)
+ .then((res) => {
+ setApiKeyValue(res["api_key"]);
+ })
+ .catch((err) => {});
+ }
+
+ return (
+
+ {children}
+
+ {title}
+
+
+
+ {renderKey === true && (
+ <>
+
+ Please save this secret key somewhere safe and accessible. For
+ security reasons,{" "}
+ you won't be able to view it again through your
+ account. If you lose this secret key, you'll need to generate a
+ new one.
+
+
+
+ {
+ setApiKeyValue(event.target.value);
+ }}
+ readOnly={true}
+ value={apiKeyValue}
+ />
+
+
+
+ {
+ handleCopyClick();
+ }}
+ >
+ {textCopied ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
+ )}
+
+ {
+ setRenderKey(true);
+ handleAddNewKey();
+ event.preventDefault();
+ }}
+ >
+ {renderKey === false && (
+
+
+
+
+ Name (optional){" "}
+
+
+
+ {
+ handleInput({ target: { name: "apikeyname", value } });
+ setApiKeyName(value);
+ }}
+ value={apiKeyName}
+ className="primary-input"
+ placeholder="My key name"
+ />
+
+
+
+ )}
+ {renderKey === false && (
+
+ {
+ setOpen(false);
+ }}
+ >
+ {cancelText}
+
+
+
+ {confirmationText}
+
+
+ )}
+
+ {renderKey === true && (
+
+ {
+ setOpen(false);
+ setRenderKey(false);
+ }}
+ className="mt-8"
+ >
+ Done
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/frontend/src/modals/UserManagementModal/index.tsx b/src/frontend/src/modals/UserManagementModal/index.tsx
index b0800decf..7f6204d66 100644
--- a/src/frontend/src/modals/UserManagementModal/index.tsx
+++ b/src/frontend/src/modals/UserManagementModal/index.tsx
@@ -1,8 +1,15 @@
import * as Form from "@radix-ui/react-form";
-import { useEffect, useState } from "react";
-import InputComponent from "../../components/inputComponent";
+import { Eye, EyeOff } from "lucide-react";
+import { useContext, useEffect, useState } from "react";
import { Button } from "../../components/ui/button";
-import { UserManagementType } from "../../types/components";
+import { Checkbox } from "../../components/ui/checkbox";
+import { CONTROL_NEW_USER } from "../../constants/constants";
+import { AuthContext } from "../../contexts/authContext";
+import {
+ UserInputType,
+ UserManagementType,
+ inputHandlerEventType,
+} from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal";
@@ -18,18 +25,32 @@ export default function UserManagementModal({
onConfirm,
}: UserManagementType) {
const Icon: any = nodeIconsLucide[icon];
-
+ const [pwdVisible, setPwdVisible] = useState(false);
+ const [confirmPwdVisible, setConfirmPwdVisible] = useState(false);
const [open, setOpen] = useState(false);
-
const [password, setPassword] = useState(data?.password ?? "");
- const [username, setUserName] = useState(data?.user ?? "");
+ const [username, setUserName] = useState(data?.username ?? "");
const [confirmPassword, setConfirmPassword] = useState(data?.password ?? "");
+ const [isActive, setIsActive] = useState(data?.is_active ?? false);
+ const [isSuperUser, setIsSuperUser] = useState(data?.is_superuser ?? false);
+ const [inputState, setInputState] = useState(CONTROL_NEW_USER);
+ const { userData } = useContext(AuthContext);
+
+ function handleInput({
+ target: { name, value },
+ }: inputHandlerEventType): void {
+ setInputState((prev) => ({ ...prev, [name]: value }));
+ }
useEffect(() => {
if (!data) {
resetForm();
+ } else {
+ handleInput({ target: { name: "username", value: username } });
+ handleInput({ target: { name: "is_active", value: isActive } });
+ handleInput({ target: { name: "is_superuser", value: isSuperUser } });
}
- }, [data, open]);
+ }, [open]);
function resetForm() {
setPassword("");
@@ -55,10 +76,8 @@ export default function UserManagementModal({
event.preventDefault();
return;
}
-
- const data = Object.fromEntries(new FormData(event.currentTarget));
resetForm();
- onConfirm(index ?? -1, data);
+ onConfirm(1, inputState);
setOpen(false);
event.preventDefault();
}}
@@ -79,8 +98,9 @@ export default function UserManagementModal({
{
- setUserName(input.target.value);
+ onChange={({ target: { value } }) => {
+ handleInput({ target: { name: "username", value } });
+ setUserName(value);
}}
value={username}
className="primary-input"
@@ -106,22 +126,40 @@ export default function UserManagementModal({
justifyContent: "space-between",
}}
>
-
+
Password{" "}
- *
+
+ *
+
+ {pwdVisible && (
+ setPwdVisible(!pwdVisible)}
+ className="h-5 cursor-pointer"
+ strokeWidth={1.5}
+ />
+ )}
+ {!pwdVisible && (
+ setPwdVisible(!pwdVisible)}
+ className="h-5 cursor-pointer"
+ strokeWidth={1.5}
+ />
+ )}
-
{
- setPassword(input);
- }}
- value={password}
- password={true}
- isForm
- className="primary-input"
- required
- placeholder="Password"
- />
+
+ {
+ handleInput({ target: { name: "password", value } });
+ setPassword(value);
+ }}
+ value={password}
+ className="primary-input"
+ required={data ? false : true}
+ type={pwdVisible ? "text" : "password"}
+ />
+
+
Please enter a password
@@ -146,93 +184,108 @@ export default function UserManagementModal({
justifyContent: "space-between",
}}
>
-
+
Confirm password{" "}
- *
+
+ *
+
+ {confirmPwdVisible && (
+
+ setConfirmPwdVisible(!confirmPwdVisible)
+ }
+ className="h-5 cursor-pointer"
+ strokeWidth={1.5}
+ />
+ )}
+ {!confirmPwdVisible && (
+
+ setConfirmPwdVisible(!confirmPwdVisible)
+ }
+ className="h-5 cursor-pointer"
+ strokeWidth={1.5}
+ />
+ )}
- {
- setConfirmPassword(input);
- }}
- value={confirmPassword}
- password={true}
- isForm
- className="primary-input"
- required
- placeholder="Confirm your password"
- />
+
+ {
+ setConfirmPassword(input.target.value);
+ }}
+ value={confirmPassword}
+ className="primary-input"
+ required={data ? false : true}
+ type={confirmPwdVisible ? "text" : "password"}
+ />
+
Please confirm your password
-
- {/*
-
-
-
- Email *
-
-
- Please enter your email
-
-
- Please provide a valid email
-
-
-
-
-
- */}
-
- {/*
-
-
-
- Date of birth{" "}
- *
-
-
- Please enter your date of birth
-
-
-
-
-
- */}
+
+
+
+
+ Active
+
+
+ {
+ handleInput({ target: { name: "is_active", value } });
+ setIsActive(value);
+ }}
+ />
+
+
+
+ {userData?.is_superuser && (
+
+
+
+ Superuser
+
+
+ {
+ handleInput({
+ target: { name: "is_superuser", value },
+ });
+ setIsSuperUser(value);
+ }}
+ />
+
+
+
+ )}
+
-
- {confirmationText}
-
{
setOpen(false);
}}
+ className="mr-3"
>
{cancelText}
+
+
+ {confirmationText}
+
diff --git a/src/frontend/src/modals/codeAreaModal/index.tsx b/src/frontend/src/modals/codeAreaModal/index.tsx
index 238641279..0dc6a2a9e 100644
--- a/src/frontend/src/modals/codeAreaModal/index.tsx
+++ b/src/frontend/src/modals/codeAreaModal/index.tsx
@@ -28,7 +28,8 @@ export default function CodeAreaModal({
const { dark } = useContext(darkContext);
const { reactFlowInstance } = useContext(typesContext);
const [height, setHeight] = useState(null);
- const { setErrorData, setSuccessData } = useContext(alertContext);
+ const { setErrorData, setSuccessData, isTweakPage } =
+ useContext(alertContext);
const [error, setError] = useState<{
detail: { error: string | undefined; traceback: string | undefined };
} | null>(null);
@@ -39,7 +40,6 @@ export default function CodeAreaModal({
if (dynamic && Object.keys(nodeClass!.template).length > 2) {
return;
}
- processCode();
}, []);
function processNonDynamicField() {
diff --git a/src/frontend/src/modals/flowSettingsModal/index.tsx b/src/frontend/src/modals/flowSettingsModal/index.tsx
index 075f0a651..48eb9225c 100644
--- a/src/frontend/src/modals/flowSettingsModal/index.tsx
+++ b/src/frontend/src/modals/flowSettingsModal/index.tsx
@@ -3,7 +3,6 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
-import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowSettingsPropsType } from "../../types/components";
import BaseModal from "../baseModal";
@@ -12,15 +11,14 @@ export default function FlowSettingsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
- const { setSuccessData } = useContext(alertContext);
const { flows, tabId, updateFlow, saveFlow } = useContext(TabsContext);
const flow = flows.find((f) => f.id === tabId);
useEffect(() => {
- setName(flow.name);
- setDescription(flow.description);
- }, [flow.name, flow.description]);
- const [name, setName] = useState(flow.name);
- const [description, setDescription] = useState(flow.description);
+ setName(flow!.name);
+ setDescription(flow!.description);
+ }, [flow!.name, flow!.description]);
+ const [name, setName] = useState(flow!.name);
+ const [description, setDescription] = useState(flow!.description);
const [invalidName, setInvalidName] = useState(false);
function handleClick(): void {
@@ -28,7 +26,6 @@ export default function FlowSettingsModal({
savedFlow!.name = name;
savedFlow!.description = description;
saveFlow(savedFlow!);
- setSuccessData({ title: "Changes saved successfully" });
setOpen(false);
}
return (
diff --git a/src/frontend/src/modals/formModal/index.tsx b/src/frontend/src/modals/formModal/index.tsx
index acca662d3..01d6a0954 100644
--- a/src/frontend/src/modals/formModal/index.tsx
+++ b/src/frontend/src/modals/formModal/index.tsx
@@ -23,6 +23,7 @@ import {
} from "../../components/ui/dialog";
import { Textarea } from "../../components/ui/textarea";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
+import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { TabsState } from "../../types/tabs";
import { validateNodes } from "../../utils/reactflowUtils";
@@ -60,6 +61,7 @@ export default function FormModal({
const [chatHistory, setChatHistory] = useState([]);
const { reactFlowInstance } = useContext(typesContext);
+ const { accessToken } = useContext(AuthContext);
const { setErrorData } = useContext(alertContext);
const ws = useRef(null);
const [lockChat, setLockChat] = useState(false);
@@ -160,7 +162,7 @@ export default function FormModal({
}, 1000);
}
}
-
+ //TODO improve check of user authentication
function getWebSocketUrl(
chatId: string,
isDevelopment: boolean = false
@@ -173,7 +175,7 @@ export default function FormModal({
return `${
isDevelopment ? "ws" : webSocketProtocol
- }://${host}${chatEndpoint}`;
+ }://${host}${chatEndpoint}?token=${accessToken}`;
}
function handleWsMessage(data: any) {
diff --git a/src/frontend/src/modals/genericModal/index.tsx b/src/frontend/src/modals/genericModal/index.tsx
index 9498bc941..e9c3e5e8b 100644
--- a/src/frontend/src/modals/genericModal/index.tsx
+++ b/src/frontend/src/modals/genericModal/index.tsx
@@ -147,7 +147,6 @@ export default function GenericModal({
}
})
.catch((error) => {
- console.log(error);
setIsEdit(true);
return setErrorData({
title: "There is something wrong with this prompt, please review it",
diff --git a/src/frontend/src/pages/AdminPage/LoginPage/index.tsx b/src/frontend/src/pages/AdminPage/LoginPage/index.tsx
index 74cc75d07..3a56b8e99 100644
--- a/src/frontend/src/pages/AdminPage/LoginPage/index.tsx
+++ b/src/frontend/src/pages/AdminPage/LoginPage/index.tsx
@@ -1,12 +1,62 @@
+import { useContext, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "../../../components/ui/button";
import { Input } from "../../../components/ui/input";
+import { CONTROL_LOGIN_STATE } from "../../../constants/constants";
+import { alertContext } from "../../../contexts/alertContext";
+import { AuthContext } from "../../../contexts/authContext";
+import { getLoggedUser, onLogin } from "../../../controllers/API";
+import { LoginType } from "../../../types/api";
+import {
+ inputHandlerEventType,
+ loginInputStateType,
+} from "../../../types/components";
export default function LoginAdminPage() {
const navigate = useNavigate();
- function loginAdmin() {
- navigate("/admin/");
+ const [inputState, setInputState] =
+ useState(CONTROL_LOGIN_STATE);
+ const { login, getAuthentication, setUserData } = useContext(AuthContext);
+
+ const { password, username } = inputState;
+ const { setErrorData } = useContext(alertContext);
+
+ function handleInput({
+ target: { name, value },
+ }: inputHandlerEventType): void {
+ setInputState((prev) => ({ ...prev, [name]: value }));
+ }
+
+ function signIn() {
+ const user: LoginType = {
+ username: username,
+ password: password,
+ };
+ onLogin(user)
+ .then((user) => {
+ login(user.access_token, user.refresh_token);
+ getUser();
+ navigate("/admin/");
+ })
+ .catch((error) => {
+ setErrorData({
+ title: "Error signing in",
+ list: [error["response"]["data"]["detail"]],
+ });
+ });
+ }
+
+ function getUser() {
+ if (getAuthentication) {
+ setTimeout(() => {
+ getLoggedUser()
+ .then((user) => {
+ setUserData(user);
+ })
+ .catch((error) => {});
+ }, 1000);
+ }
}
return (
@@ -14,11 +64,24 @@ export default function LoginAdminPage() {
⛓️
Admin
-
-
+
{
+ handleInput({ target: { name: "username", value } });
+ }}
+ className="bg-background"
+ placeholder="Username"
+ />
+
{
+ handleInput({ target: { name: "password", value } });
+ }}
+ className="bg-background"
+ placeholder="Password"
+ />
{
- loginAdmin();
+ signIn();
}}
variant="default"
className="w-full"
diff --git a/src/frontend/src/pages/AdminPage/index.tsx b/src/frontend/src/pages/AdminPage/index.tsx
index e598af8b4..fc963fee8 100644
--- a/src/frontend/src/pages/AdminPage/index.tsx
+++ b/src/frontend/src/pages/AdminPage/index.tsx
@@ -1,10 +1,11 @@
-import _ from "lodash";
+import { cloneDeep } from "lodash";
import { X } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
+import { useContext, useEffect, useRef, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
+import { Checkbox } from "../../components/ui/checkbox";
import { Input } from "../../components/ui/input";
import {
Table,
@@ -14,265 +15,202 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
+import { alertContext } from "../../contexts/alertContext";
+import { AuthContext } from "../../contexts/authContext";
+import {
+ addUser,
+ deleteUser,
+ getUsersPage,
+ updateUser,
+} from "../../controllers/API";
import ConfirmationModal from "../../modals/ConfirmationModal";
import UserManagementModal from "../../modals/UserManagementModal";
+import { UserInputType } from "../../types/components";
+import Header from "../../components/headerComponent";
+import { Users } from "../../types/api";
export default function AdminPage() {
const [inputValue, setInputValue] = useState("");
const [size, setPageSize] = useState(10);
- const [index, setPageIndex] = useState(1);
+ const [index, setPageIndex] = useState(0);
+ const [loadingUsers, setLoadingUsers] = useState(true);
+ const { setErrorData, setSuccessData } = useContext(alertContext);
+ const { userData } = useContext(AuthContext);
+ const [totalRowsCount, setTotalRowsCount] = useState(0);
- const userList = useRef([
- {
- user: generateRandomString(50),
- email: generateRandomString(50) + "@example.com",
- password: generateRandomString(50),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- {
- user: generateRandomString(8),
- email: generateRandomString(10) + "@example.com",
- password: generateRandomString(12),
- register_date: generateRandomDate(),
- },
- ]);
+ const userList = useRef([]);
+
+ useEffect(() => {
+ setTimeout(() => {
+ getUsers();
+ }, 500);
+ }, []);
const [filterUserList, setFilterUserList] = useState(userList.current);
- function generateRandomString(length) {
- const characters =
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- let result = "";
- for (let i = 0; i < length; i++) {
- const randomIndex = Math.floor(Math.random() * characters.length);
- result += characters.charAt(randomIndex);
- }
- return result;
+ function getUsers() {
+ setLoadingUsers(true);
+ getUsersPage(index, size)
+ .then((users) => {
+ setTotalRowsCount(users["total_count"]);
+ userList.current = users["users"];
+ setFilterUserList(users["users"]);
+ setLoadingUsers(false);
+ })
+ .catch((error) => {
+ setLoadingUsers(false);
+ });
}
- function generateRandomDate() {
- const start = new Date(2010, 0, 1);
- const end = new Date();
- const randomTimestamp =
- start.getTime() + Math.random() * (end.getTime() - start.getTime());
- const randomDate = new Date(randomTimestamp);
-
- const options = { year: "numeric", month: "short", day: "numeric" };
- return randomDate.toLocaleDateString("en-US");
- }
-
- const [editUser, setEditUser] = useState(-1);
- const [editedUser, setEditedUser] = useState("");
- const [modalEditOpen, setModalEditOpen] = useState(false);
- const [modalDeleteOpen, setModalDeleteOpen] = useState(false);
-
- useEffect(() => {
- resetFilter();
- }, []);
-
- const handleInputChange = (event, index) => {
- const user = _.cloneDeepWith(userList.current);
- user[index].password = event.target.value;
- userList.current = user;
-
- const userFilter = _.cloneDeepWith(filterUserList);
- userFilter[index].password = event.target.value;
- setFilterUserList(userFilter);
-
- setEditedUser(event.target.value);
- };
-
function handleChangePagination(pageIndex: number, pageSize: number) {
- setPageIndex(pageIndex);
- setPageSize(pageSize);
-
- const startIndex = (pageIndex - 1) * pageSize;
- const endIndex = startIndex + pageSize;
- const newList = userList.current.slice(startIndex, endIndex);
-
- setFilterUserList(newList);
+ setLoadingUsers(true);
+ getUsersPage(pageIndex, pageSize)
+ .then((users) => {
+ setTotalRowsCount(users["total_count"]);
+ userList.current = users["users"];
+ setFilterUserList(users["users"]);
+ setLoadingUsers(false);
+ })
+ .catch((error) => {
+ setLoadingUsers(false);
+ });
}
function resetFilter() {
- setFilterUserList(userList.current);
- setPageIndex(1);
+ setPageIndex(0);
setPageSize(10);
-
- const startIndex = (index - 1) * size;
- const endIndex = index + size - 1;
- const newList = userList.current.slice(startIndex, endIndex);
-
- console.log(userList.current);
-
- setFilterUserList(newList);
+ getUsers();
}
function handleFilterUsers(input: string) {
setInputValue(input);
if (input === "") {
- resetFilter();
+ setFilterUserList(userList.current);
} else {
- const filteredList = userList.current.filter(
- (user) =>
- user.user.toLowerCase().includes(input.toLowerCase()) ||
- user.email.toLowerCase().includes(input.toLowerCase())
+ const filteredList = userList.current.filter((user:Users) =>
+ user.username.toLowerCase().includes(input.toLowerCase())
);
setFilterUserList(filteredList);
}
}
- function handleDeleteUser(index) {
- const user = _.cloneDeepWith(userList.current);
- user.splice(index, 1);
- userList.current = user;
-
- const userFilter = _.cloneDeepWith(filterUserList);
- userFilter.splice(index, 1);
- setFilterUserList(userFilter);
-
- resetFilter();
+ function handleDeleteUser(user) {
+ deleteUser(user.id)
+ .then((res) => {
+ resetFilter();
+ setSuccessData({
+ title: "Success! User deleted!",
+ });
+ })
+ .catch((error) => {
+ setErrorData({
+ title: "Error on delete user",
+ list: [error["response"]["data"]["detail"]],
+ });
+ });
}
- function handleEditUser(index, user) {
- const newUser = _.cloneDeepWith(userList.current);
- newUser[index].password = user.password;
- newUser[index].user = user.username;
- userList.current = newUser;
- resetFilter();
+ function handleEditUser(userId, user) {
+ updateUser(userId, user)
+ .then((res) => {
+ resetFilter();
+ setSuccessData({
+ title: "Success! User edited!",
+ });
+ })
+ .catch((error) => {
+ setErrorData({
+ title: "Error on edit user",
+ list: [error["response"]["data"]["detail"]],
+ });
+ });
}
- function handleNewUser(user) {
- const newUser = {
- user: user.username,
- email: generateRandomString(50) + "@example.com",
- password: user.password,
- register_date: generateRandomDate(),
- };
+ function handleDisableUser(check, userId, user) {
+ const userEdit = cloneDeep(user);
+ userEdit.is_active = !check;
- userList.current.unshift(newUser);
- console.log(userList.current);
+ updateUser(userId, userEdit)
+ .then((res) => {
+ resetFilter();
+ setSuccessData({
+ title: "Success! User edited!",
+ });
+ })
+ .catch((error) => {
+ setErrorData({
+ title: "Error on edit user",
+ list: [error["response"]["data"]["detail"]],
+ });
+ });
+ }
- resetFilter();
+ function handleSuperUserEdit(check, userId, user) {
+ const userEdit = cloneDeep(user);
+ userEdit.is_superuser = !check;
+ updateUser(userId, userEdit)
+ .then((res) => {
+ resetFilter();
+ setSuccessData({
+ title: "Success! User edited!",
+ });
+ })
+ .catch((error) => {
+ setErrorData({
+ title: "Error on edit user",
+ list: [error["response"]["data"]["detail"]],
+ });
+ });
+ }
+
+ function handleNewUser(user: UserInputType) {
+ addUser(user)
+ .then((res) => {
+ resetFilter();
+ setSuccessData({
+ title: "Success! New user added!",
+ });
+ })
+ .catch((error) => {
+ setErrorData({
+ title: "Error on add new user",
+ list: [error["response"]["data"]["detail"]],
+ });
+ });
}
return (
<>
-
-
-
-
-
-
-
- Welcome back!
-
-
- Here's a list of all users!
-
-
-
-
-
- {userList.current.length === 0 && (
- <>
-
-
There's no users left :)
+
+
+ {userData && (
+
+
+
+
+
+
+
+ Welcome back!
+
+
+ Navigate through this section to efficiently oversee all
+ application users. From here, you can seamlessly manage
+ user accounts.
+
- >
- )}
- {userList.current.length > 0 && (
+
+
+
+ {userList.current.length === 0 && !loadingUsers && (
+ <>
+
+
There's no users registered :)
+
+ >
+ )}
<>
@@ -285,8 +223,8 @@ export default function AdminPage() {
{inputValue.length > 0 && (
{
- resetFilter();
setInputValue("");
+ setFilterUserList(userList.current);
}}
variant="ghost"
className="h-8 px-2 lg:px-3"
@@ -311,90 +249,183 @@ export default function AdminPage() {
+ {loadingUsers && (
+
+ Loading...
+
+ )}
-
-
+
+
- User
- Password
+ Id
+ Username
+ Active
+ Superuser
+ Created At
+ Updated At
-
- {filterUserList.map((user, index) => (
-
-
- {user.user}
-
-
- {user.password}
-
-
-
-
{
- handleEditUser(index, user);
- }}
- >
-
-
-
-
-
+ {!loadingUsers && (
+
+ {filterUserList.map((user:UserInputType, index) => (
+
+
+
+
+ {user.id}
+
+
+
+
+
+
+ {user.username}
+
+
+
+
{
- handleDeleteUser(index);
+ handleDisableUser(
+ user.is_active,
+ user.id,
+ user
+ );
}}
>
-
-
-
+
-
-
-
- ))}
-
+
+
+ {
+ handleSuperUserEdit(
+ user.is_superuser,
+ user.id,
+ user
+ );
+ }}
+ >
+
+
+
+
+ {
+ new Date(user.create_at!)
+ .toISOString()
+ .split("T")[0]
+ }
+
+
+ {
+ new Date(user.updated_at!)
+ .toISOString()
+ .split("T")[0]
+ }
+
+
+
+ {
+ handleEditUser(user.id, editUser);
+ }}
+ >
+
+
+
+
+
+ {
+ handleDeleteUser(user);
+ }}
+ >
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
{
handleChangePagination(pageSize, pageIndex);
}}
>
>
- )}
+
+ )}
>
);
diff --git a/src/frontend/src/pages/ApiKeysPage/index.tsx b/src/frontend/src/pages/ApiKeysPage/index.tsx
new file mode 100644
index 000000000..d1c91593a
--- /dev/null
+++ b/src/frontend/src/pages/ApiKeysPage/index.tsx
@@ -0,0 +1,283 @@
+import { useContext, useEffect, useRef, useState } from "react";
+import ShadTooltip from "../../components/ShadTooltipComponent";
+import IconComponent from "../../components/genericIconComponent";
+import { Button } from "../../components/ui/button";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "../../components/ui/table";
+import { alertContext } from "../../contexts/alertContext";
+import { AuthContext } from "../../contexts/authContext";
+import { deleteApiKey, getApiKey } from "../../controllers/API";
+import ConfirmationModal from "../../modals/ConfirmationModal";
+import SecretKeyModal from "../../modals/SecretKeyModal";
+
+import moment from "moment";
+import Header from "../../components/headerComponent";
+import {
+ API_PAGE_PARAGRAPH_1,
+ API_PAGE_PARAGRAPH_2,
+ API_PAGE_USER_KEYS,
+ LAST_USED_SPAN_1,
+ LAST_USED_SPAN_2,
+} from "../../constants/constants";
+import { ApiKey } from "../../types/components";
+
+export default function ApiKeysPage() {
+ const [loadingKeys, setLoadingKeys] = useState(true);
+ const { setErrorData, setSuccessData } = useContext(alertContext);
+ const { userData } = useContext(AuthContext);
+ const [userId, setUserId] = useState("");
+ const keysList = useRef([]);
+
+ useEffect(() => {
+ getKeys();
+ }, [userData]);
+
+ function getKeys() {
+ setLoadingKeys(true);
+ if (userData) {
+ getApiKey()
+ .then((keys: [ApiKey]) => {
+ keysList.current = keys["api_keys"];
+ setUserId(keys["user_id"]);
+ setLoadingKeys(false);
+ })
+ .catch((error) => {
+ setLoadingKeys(false);
+ });
+ }
+ }
+
+ function resetFilter() {
+ getKeys();
+ }
+
+ function handleDeleteKey(keys) {
+ deleteApiKey(keys)
+ .then((res) => {
+ resetFilter();
+ setSuccessData({
+ title: "Success! Key deleted!",
+ });
+ })
+ .catch((error) => {
+ setErrorData({
+ title: "Error on delete key",
+ list: [error["response"]["data"]["detail"]],
+ });
+ });
+ }
+
+ function lastUsedMessage() {
+ return (
+
+
+ {LAST_USED_SPAN_1}
+ {LAST_USED_SPAN_2}
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {userData && (
+
+
+
+
+
+
+
+ API keys
+
+
+ {API_PAGE_PARAGRAPH_1}
+
+ {API_PAGE_PARAGRAPH_2}
+
+
+
+
+
+ {keysList.current &&
+ keysList.current.length === 0 &&
+ !loadingKeys && (
+ <>
+
+
{API_PAGE_USER_KEYS}
+
+ >
+ )}
+ <>
+ {loadingKeys && (
+
+ Loading...
+
+ )}
+
+ {keysList.current &&
+ keysList.current.length > 0 &&
+ !loadingKeys && (
+
+
+
+ Name
+ Key
+ Created
+
+ Last Used
+
+
+
+
+
+
+ Total Uses
+
+
+
+ {!loadingKeys && (
+
+ {keysList.current.map(
+ (api_keys: ApiKey, index: number) => (
+
+
+
+
+ {api_keys.name ? api_keys.name : "-"}
+
+
+
+
+
+ {api_keys.api_key}
+
+
+
+
+
+ {moment(api_keys.created_at).format(
+ "YYYY-MM-DD HH:mm"
+ )}
+
+
+
+
+
+
+ {moment(api_keys.last_used_at).format(
+ "YYYY-MM-DD HH:mm"
+ ) === "Invalid date"
+ ? "Never"
+ : moment(
+ api_keys.last_used_at
+ ).format("YYYY-MM-DD HH:mm")}
+
+
+
+
+ {api_keys.total_uses}
+
+
+
+ {
+ handleDeleteKey(keys);
+ }}
+ >
+
+
+
+
+
+
+
+ )
+ )}
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+ Create new secret key
+
+
+
+
+ >
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx
index 30e3865d7..74a211c33 100644
--- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx
@@ -431,6 +431,7 @@ export default function Page({
zoomOnScroll={!view}
zoomOnPinch={!view}
panOnDrag={!view}
+ proOptions={{hideAttribution: true}}
>
{!view && (
diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx
index f8ac36230..c506975ae 100644
--- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx
@@ -21,7 +21,8 @@ export default function ExtraSidebar(): JSX.Element {
const { data, templates } = useContext(typesContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
useContext(TabsContext);
- const { setSuccessData, setErrorData } = useContext(alertContext);
+ const { setSuccessData, setErrorData, setIsTweakPage } =
+ useContext(alertContext);
const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState("");
const isPending = tabsState[tabId]?.isPending;
@@ -100,7 +101,10 @@ export default function ExtraSidebar(): JSX.Element {
{flow && flow.data && (
-
+
setIsTweakPage(true)}
+ >
{
saveFlow(flow!);
- setSuccessData({ title: "Changes saved successfully" });
}}
>
{
- setTabId(id);
+ setTabId(id!);
}, [id]);
// Initialize state variable for the version
@@ -26,7 +26,7 @@ export default function ViewPage() {
{flows.length > 0 &&
tabId !== "" &&
flows.findIndex((flow) => flow.id === tabId) !== -1 && (
- flow.id === tabId)} />
+ flow.id === tabId)!} />
)}
);
diff --git a/src/frontend/src/pages/loginPage/index.tsx b/src/frontend/src/pages/loginPage/index.tsx
index 8cba8bc60..14e04708b 100644
--- a/src/frontend/src/pages/loginPage/index.tsx
+++ b/src/frontend/src/pages/loginPage/index.tsx
@@ -1,10 +1,14 @@
import * as Form from "@radix-ui/react-form";
-import { useState } from "react";
-import { Link } from "react-router-dom";
+import { useContext, useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
import InputComponent from "../../components/inputComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { CONTROL_LOGIN_STATE } from "../../constants/constants";
+import { alertContext } from "../../contexts/alertContext";
+import { AuthContext } from "../../contexts/authContext";
+import { getLoggedUser, onLogin } from "../../controllers/API";
+import { LoginType } from "../../types/api";
import {
inputHandlerEventType,
loginInputStateType,
@@ -15,12 +19,49 @@ export default function LoginPage(): JSX.Element {
useState
(CONTROL_LOGIN_STATE);
const { password, username } = inputState;
+ const { login, getAuthentication, setUserData, setIsAdmin } = useContext(AuthContext);
+ const navigate = useNavigate();
+ const { setErrorData } = useContext(alertContext);
function handleInput({
target: { name, value },
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
+
+ function signIn() {
+ const user: LoginType = {
+ username: username,
+ password: password,
+ };
+ onLogin(user)
+ .then((user) => {
+ login(user.access_token, user.refresh_token);
+ getUser();
+ navigate("/");
+ })
+ .catch((error) => {
+ setErrorData({
+ title: "Error signing in",
+ list: [error["response"]["data"]["detail"]],
+ });
+ });
+ }
+
+ function getUser() {
+ if (getAuthentication()) {
+ setTimeout(() => {
+ getLoggedUser()
+ .then((user) => {
+ const isSuperUser = user.is_superuser;
+ setIsAdmin(isSuperUser);
+ setUserData(user);
+ })
+ .catch((error) => {});
+ }, 500);
+ }
+ }
+
return (
{
@@ -28,7 +69,7 @@ export default function LoginPage(): JSX.Element {
event.preventDefault();
return;
}
-
+ signIn();
const data = Object.fromEntries(new FormData(event.currentTarget));
event.preventDefault();
}}
@@ -92,7 +133,7 @@ export default function LoginPage(): JSX.Element {
-
+
Don't have an account? Sign Up
diff --git a/src/frontend/src/pages/signUpPage/index.tsx b/src/frontend/src/pages/signUpPage/index.tsx
index c81253f4d..92f3eff97 100644
--- a/src/frontend/src/pages/signUpPage/index.tsx
+++ b/src/frontend/src/pages/signUpPage/index.tsx
@@ -1,11 +1,17 @@
import * as Form from "@radix-ui/react-form";
-import { FormEvent, useState } from "react";
-import { Link } from "react-router-dom";
+import { FormEvent, useContext, useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
import InputComponent from "../../components/inputComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
-import { CONTROL_INPUT_STATE } from "../../constants/constants";
import {
+ CONTROL_INPUT_STATE,
+ SIGN_UP_SUCCESS,
+} from "../../constants/constants";
+import { alertContext } from "../../contexts/alertContext";
+import { addUser } from "../../controllers/API";
+import {
+ UserInputType,
inputHandlerEventType,
signUpInputStateType,
} from "../../types/components";
@@ -15,12 +21,42 @@ export default function SignUp(): JSX.Element {
useState(CONTROL_INPUT_STATE);
const { password, cnfPassword, username } = inputState;
+ const { setErrorData, setSuccessData } = useContext(alertContext);
+ const navigate = useNavigate();
function handleInput({
target: { name, value },
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
+
+ function handleSignup(): void {
+ const { username, password } = inputState;
+ const newUser: UserInputType = {
+ username,
+ password,
+ };
+ addUser(newUser)
+ .then((user) => {
+ setSuccessData({
+ title: SIGN_UP_SUCCESS,
+ });
+ navigate("/login");
+ })
+ .catch((error) => {
+ const {
+ response: {
+ data: { detail },
+ },
+ } = error;
+ setErrorData({
+ title: "Error signing up",
+ list: [detail],
+ });
+ return;
+ });
+ }
+
return (
) => {
@@ -120,7 +156,14 @@ export default function SignUp(): JSX.Element {
- Sign up
+ {
+ handleSignup();
+ }}
+ >
+ Sign up
+
diff --git a/src/frontend/src/routes.tsx b/src/frontend/src/routes.tsx
index 0d023e360..cd8f86c9a 100644
--- a/src/frontend/src/routes.tsx
+++ b/src/frontend/src/routes.tsx
@@ -1,32 +1,116 @@
import { Route, Routes } from "react-router-dom";
+import { ProtectedAdminRoute } from "./components/authAdminGuard";
+import { ProtectedRoute } from "./components/authGuard";
+import { ProtectedLoginRoute } from "./components/authLoginGuard";
+import { CatchAllRoute } from "./components/catchAllRoutes";
import AdminPage from "./pages/AdminPage";
import LoginAdminPage from "./pages/AdminPage/LoginPage";
+import ApiKeysPage from "./pages/ApiKeysPage";
import CommunityPage from "./pages/CommunityPage";
import FlowPage from "./pages/FlowPage";
import HomePage from "./pages/MainPage";
import ViewPage from "./pages/ViewPage";
import DeleteAccountPage from "./pages/deleteAccountPage";
import LoginPage from "./pages/loginPage";
+import SignUp from "./pages/signUpPage";
const Router = () => {
return (
- } />
- } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
- } />
- } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
- } />
+
+
+
+ }
+ />
- } />
- {/* } /> */}
- } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
- } />
+
+
+
+ }
+ />
- }>
+
+
+
+ }
+ >
+
+
+
+ }
+ >
);
diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css
index c10a85f02..4f6f9b3a5 100644
--- a/src/frontend/src/style/applies.css
+++ b/src/frontend/src/style/applies.css
@@ -488,10 +488,10 @@
@apply flex-max-width h-12 items-center justify-between border-border bg-muted;
}
.header-start-display {
- @apply flex w-96 items-center justify-start gap-2;
+ @apply flex w-[30%] items-center justify-start gap-2;
}
.header-end-division {
- @apply flex w-96 justify-end px-2;
+ @apply flex w-[30%] justify-end px-2;
}
.header-end-display {
@apply ml-auto mr-2 flex items-center gap-5;
diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts
index 5a5701b2d..b690d7134 100644
--- a/src/frontend/src/types/api/index.ts
+++ b/src/frontend/src/types/api/index.ts
@@ -62,3 +62,27 @@ export type UploadFileTypeAPI = {
file_path: string;
flowId: string;
};
+
+export type LoginType = {
+ grant_type?: string;
+ username: string;
+ password: string;
+ scrope?: string;
+ client_id?: string;
+ client_secret?: string;
+};
+
+export type LoginAuthType = {
+ access_token: string;
+ refresh_token: string;
+ token_type?: string;
+};
+
+export type Users = {
+ id: string;
+ username: string;
+ is_active: boolean;
+ is_superuser: boolean;
+ create_at: Date;
+ updated_at: Date;
+};
diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts
index 1c80c5df4..6c9861fd9 100644
--- a/src/frontend/src/types/components/index.ts
+++ b/src/frontend/src/types/components/index.ts
@@ -218,7 +218,7 @@ export type signUpInputStateType = {
export type inputHandlerEventType = {
target: {
- value: string;
+ value: string | boolean;
name: string;
};
};
@@ -261,6 +261,29 @@ export type loginInputStateType = {
password: string;
};
+export type UserInputType = {
+ username: string;
+ password: string;
+ is_active?: boolean;
+ is_superuser?: boolean;
+ id?: string;
+ create_at?: string;
+ updated_at?:string;
+};
+
+export type ApiKeyType = {
+ title: string;
+ cancelText: string;
+ confirmationText: string;
+ children: ReactElement;
+ icon: string;
+ data?: any;
+ onCloseModal: () => void;
+};
+
+export type ApiKeyInputType = {
+ apikeyname: string;
+};
export type groupedObjType = {
family: string;
type: string;
@@ -508,3 +531,16 @@ export type validationStatusType = {
progress: number;
valid: boolean;
};
+
+export type ApiKey = {
+ id: string;
+ api_key: string;
+ name: string;
+ created_at: string;
+ last_used_at: string;
+ total_uses: number;
+};
+export type fetchErrorComponentType = {
+ message: string;
+ description: string;
+};
diff --git a/src/frontend/src/types/contexts/auth.ts b/src/frontend/src/types/contexts/auth.ts
index af037ecae..4946efa44 100644
--- a/src/frontend/src/types/contexts/auth.ts
+++ b/src/frontend/src/types/contexts/auth.ts
@@ -1,16 +1,17 @@
+import { Users } from "../api";
+
export type AuthContextType = {
+ isAdmin: boolean;
+ setIsAdmin: (isAdmin: boolean) => void;
isAuthenticated: boolean;
accessToken: string | null;
+ refreshToken: string | null;
login: (accessToken: string, refreshToken: string) => void;
logout: () => void;
- refreshAccessToken: (refreshToken: string) => Promise
;
- userData: userData | null;
- setUserData: (userData: userData | null) => void;
-};
-
-export type userData = {
- id: string;
- name: string;
- email: string;
- role: string;
+ userData: Users | null;
+ setUserData: (userData: Users | null) => void;
+ getAuthentication: () => boolean;
+ authenticationErrorCount: number;
+ autoLogin: boolean;
+ setAutoLogin: (autoLogin: boolean) => void;
};
diff --git a/src/frontend/src/types/typesContext/index.ts b/src/frontend/src/types/typesContext/index.ts
index 9e57822b9..a7f993a7a 100644
--- a/src/frontend/src/types/typesContext/index.ts
+++ b/src/frontend/src/types/typesContext/index.ts
@@ -16,6 +16,8 @@ export type typesContextType = {
setTemplates: (newState: {}) => void;
data: APIDataType;
setData: (newState: {}) => void;
+ fetchError: boolean;
+ setFetchError: (newState: boolean) => void;
};
export type alertContextType = {
@@ -39,11 +41,15 @@ export type alertContextType = {
removeFromNotificationList: (index: string) => void;
loading: boolean;
setLoading: (newState: boolean) => void;
+ isTweakPage: boolean;
+ setIsTweakPage: (newState: boolean) => void;
};
export type darkContextType = {
dark: {};
setDark: (newState: {}) => void;
+ stars: number;
+ setStars: (stars: number) => void;
};
export type locationContextType = {
diff --git a/src/frontend/src/types/utils/reactflowUtils.ts b/src/frontend/src/types/utils/reactflowUtils.ts
index 40a3f2868..ca2bc70a8 100644
--- a/src/frontend/src/types/utils/reactflowUtils.ts
+++ b/src/frontend/src/types/utils/reactflowUtils.ts
@@ -1,4 +1,4 @@
-import { Edge } from "reactflow";
+import { Edge, Node } from "reactflow";
import { NodeType } from "../flow";
export type cleanEdgesType = {
@@ -9,6 +9,11 @@ export type cleanEdgesType = {
updateEdge: (edge: Edge[]) => void;
};
+export type unselectAllNodesType = {
+ updateNodes: (nodes: Node[]) => void;
+ data: Node[] | null;
+};
+
export type updateEdgesHandleIdsType = {
nodes: NodeType[];
edges: Edge[];
diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts
index e11d84471..9aec3f518 100644
--- a/src/frontend/src/utils/reactflowUtils.ts
+++ b/src/frontend/src/utils/reactflowUtils.ts
@@ -2,6 +2,7 @@ import _ from "lodash";
import {
Connection,
Edge,
+ Node,
ReactFlowInstance,
ReactFlowJsonObject,
} from "reactflow";
@@ -16,6 +17,7 @@ import {
import {
cleanEdgesType,
updateEdgesHandleIdsType,
+ unselectAllNodesType,
} from "../types/utils/reactflowUtils";
import { toNormalCase } from "./utils";
@@ -64,7 +66,14 @@ export function cleanEdges({
updateEdge(newEdges);
}
-// add comments to this function
+export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
+ let newNodes = _.cloneDeep(data);
+ newNodes!.forEach((node: Node) => {
+ node.selected = false;
+ });
+ updateNodes(newNodes!);
+}
+
export function isValidConnection(
{ source, target, sourceHandle, targetHandle }: Connection,
reactFlowInstance: ReactFlowInstance
diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts
index ffeb4c650..31a7adf7e 100644
--- a/src/frontend/src/utils/styleUtils.ts
+++ b/src/frontend/src/utils/styleUtils.ts
@@ -19,6 +19,8 @@ import {
Edit,
Eraser,
ExternalLink,
+ Eye,
+ EyeOff,
File,
FileDown,
FileSearch,
@@ -33,6 +35,7 @@ import {
HelpCircle,
Home,
Info,
+ Key,
Laptop2,
Layers,
Lightbulb,
@@ -59,7 +62,9 @@ import {
TerminalSquare,
Trash2,
Undo,
+ Unplug,
Upload,
+ UserCog2,
UserMinus2,
UserPlus2,
Users2,
@@ -290,4 +295,9 @@ export const nodeIconsLucide: iconsType = {
ChevronsLeft,
FaGithub,
FaApple,
+ EyeOff,
+ Eye,
+ UserCog2,
+ Key,
+ Unplug,
};
diff --git a/src/frontend/tailwind.config.js b/src/frontend/tailwind.config.js
index 52330ae92..5130f3fcf 100644
--- a/src/frontend/tailwind.config.js
+++ b/src/frontend/tailwind.config.js
@@ -201,6 +201,12 @@ module.exports = {
".dark .theme-attribution .react-flow__attribution a": {
color: "black",
},
+ ".text-align-last-left": {
+ "text-align-last": "left",
+ },
+ ".text-align-last-right": {
+ "text-align-last": "right",
+ },
});
}),
require("@tailwindcss/typography"),
diff --git a/tests/conftest.py b/tests/conftest.py
index e90d03d0a..1d1fb9ac7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,6 +5,9 @@ from typing import AsyncGenerator, TYPE_CHECKING
from langflow.api.v1.flows import get_session
from langflow.graph.graph.base import Graph
+from langflow.services.auth.utils import get_password_hash
+from langflow.services.database.models.flow.flow import Flow
+from langflow.services.database.models.user.user import User, UserCreate
import pytest
from fastapi.testclient import TestClient
from httpx import AsyncClient
@@ -43,7 +46,7 @@ async def async_client() -> AsyncGenerator:
# Create client fixture for FastAPI
-@pytest.fixture(scope="module")
+@pytest.fixture(scope="module", autouse=True)
def client():
from langflow.main import create_app
@@ -155,3 +158,53 @@ def session_getter_fixture(client):
@pytest.fixture
def runner():
return CliRunner()
+
+
+@pytest.fixture
+def test_user(client):
+ user_data = UserCreate(
+ username="testuser",
+ password="testpassword",
+ )
+ response = client.post("/api/v1/user", json=user_data.dict())
+ return response.json()
+
+
+@pytest.fixture(scope="function")
+def active_user(client, session):
+ user = User(
+ username="activeuser",
+ password=get_password_hash(
+ "testpassword"
+ ), # Assuming password needs to be hashed
+ is_active=True,
+ is_superuser=False,
+ )
+ session.add(user)
+ session.commit()
+ return user
+
+
+@pytest.fixture
+def logged_in_headers(client, active_user):
+ login_data = {"username": active_user.username, "password": "testpassword"}
+ response = client.post("/api/v1/login", data=login_data)
+ assert response.status_code == 200
+ tokens = response.json()
+ a_token = tokens["access_token"]
+ return {"Authorization": f"Bearer {a_token}"}
+
+
+@pytest.fixture
+def flow(client, json_flow: str, session, active_user):
+ from langflow.services.database.models.flow.flow import FlowCreate
+
+ loaded_json = json.loads(json_flow)
+ flow_data = FlowCreate(
+ name="test_flow", data=loaded_json.get("data"), user_id=active_user.id
+ )
+ flow = Flow(**flow_data.dict())
+ session.add(flow)
+ session.commit()
+
+ return flow
diff --git a/tests/test_agents_template.py b/tests/test_agents_template.py
index 0b5fb7c3a..b12ad7dee 100644
--- a/tests/test_agents_template.py
+++ b/tests/test_agents_template.py
@@ -1,8 +1,8 @@
from fastapi.testclient import TestClient
-def test_zero_shot_agent(client: TestClient):
- response = client.get("api/v1/all")
+def test_zero_shot_agent(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
agents = json_response["agents"]
@@ -113,8 +113,8 @@ def test_zero_shot_agent(client: TestClient):
}
-def test_json_agent(client: TestClient):
- response = client.get("api/v1/all")
+def test_json_agent(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
agents = json_response["agents"]
@@ -152,8 +152,8 @@ def test_json_agent(client: TestClient):
}
-def test_csv_agent(client: TestClient):
- response = client.get("api/v1/all")
+def test_csv_agent(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
agents = json_response["agents"]
@@ -195,8 +195,8 @@ def test_csv_agent(client: TestClient):
}
-def test_initialize_agent(client: TestClient):
- response = client.get("api/v1/all")
+def test_initialize_agent(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
agents = json_response["agents"]
diff --git a/tests/test_api_key.py b/tests/test_api_key.py
new file mode 100644
index 000000000..43b91fa43
--- /dev/null
+++ b/tests/test_api_key.py
@@ -0,0 +1,50 @@
+import pytest
+from langflow.services.database.models.api_key import ApiKeyCreate
+
+
+@pytest.fixture
+def api_key(client, logged_in_headers, active_user):
+ api_key = ApiKeyCreate(name="test-api-key")
+
+ response = client.post(
+ "api/v1/api_key", data=api_key.json(), headers=logged_in_headers
+ )
+ assert response.status_code == 200, response.text
+ return response.json()
+
+
+def test_get_api_keys(client, logged_in_headers, api_key):
+ response = client.get("api/v1/api_key", headers=logged_in_headers)
+ assert response.status_code == 200, response.text
+ data = response.json()
+ assert "total_count" in data
+ assert "user_id" in data
+ assert "api_keys" in data
+ assert any("test-api-key" in api_key["name"] for api_key in data["api_keys"])
+ # assert all api keys in data["api_keys"] are masked
+ assert all("**" in api_key["api_key"] for api_key in data["api_keys"])
+ # Add more assertions as needed based on the expected data structure and content
+
+
+def test_create_api_key(client, logged_in_headers):
+ api_key_name = "test-api-key"
+ response = client.post(
+ "api/v1/api_key", json={"name": api_key_name}, headers=logged_in_headers
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert "name" in data and data["name"] == api_key_name
+ assert "api_key" in data
+ # When creating the API key is returned which is
+ # the only time the API key is unmasked
+ assert "**" not in data["api_key"]
+
+
+def test_delete_api_key(client, logged_in_headers, active_user, api_key):
+ # Assuming a function to create a test API key, returning the key ID
+ api_key_id = api_key["id"]
+ response = client.delete(f"api/v1/api_key/{api_key_id}", headers=logged_in_headers)
+ assert response.status_code == 200
+ data = response.json()
+ assert data["detail"] == "API Key deleted"
+ # Optionally, add a follow-up check to ensure that the key is actually removed from the database
diff --git a/tests/test_cache.py b/tests/test_cache.py
index 50698c304..edf205a05 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -1,4 +1,6 @@
import json
+from langflow.services.database.models.base import orjson_dumps
+import orjson
from langflow.graph import Graph
import pytest
@@ -63,9 +65,9 @@ def test_cache_size_limit(basic_data_graph):
nodes = modified_data_graph["nodes"]
node_id = nodes[0]["id"]
# Now we replace all instances ode node_id with a new id in the json
- json_string = json.dumps(modified_data_graph)
+ json_string = orjson_dumps(modified_data_graph)
modified_json_string = json_string.replace(node_id, f"{node_id}_{i}")
- modified_data_graph_new_id = json.loads(modified_json_string)
+ modified_data_graph_new_id = orjson.loads(modified_json_string)
build_langchain_object_with_caching(modified_data_graph_new_id)
assert len(build_langchain_object_with_caching.cache) == 10
diff --git a/tests/test_chains_template.py b/tests/test_chains_template.py
index 4339dbe3b..eb20a0571 100644
--- a/tests/test_chains_template.py
+++ b/tests/test_chains_template.py
@@ -1,8 +1,8 @@
from fastapi.testclient import TestClient
-# def test_chains_settings(client: TestClient):
-# response = client.get("api/v1/all")
+# def test_chains_settings(client: TestClient, logged_in_headers):
+# response = client.get("api/v1/all", headers=logged_in_headers)
# assert response.status_code == 200
# json_response = response.json()
# chains = json_response["chains"]
@@ -10,8 +10,8 @@ from fastapi.testclient import TestClient
# Test the ConversationChain object
-def test_conversation_chain(client: TestClient):
- response = client.get("api/v1/all")
+def test_conversation_chain(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
chains = json_response["chains"]
@@ -102,8 +102,8 @@ def test_conversation_chain(client: TestClient):
)
-def test_llm_chain(client: TestClient):
- response = client.get("api/v1/all")
+def test_llm_chain(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
chains = json_response["chains"]
@@ -173,8 +173,8 @@ def test_llm_chain(client: TestClient):
}
-def test_llm_checker_chain(client: TestClient):
- response = client.get("api/v1/all")
+def test_llm_checker_chain(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
chains = json_response["chains"]
@@ -207,8 +207,8 @@ def test_llm_checker_chain(client: TestClient):
assert chain["description"] == ""
-def test_llm_math_chain(client: TestClient):
- response = client.get("api/v1/all")
+def test_llm_math_chain(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
chains = json_response["chains"]
@@ -299,8 +299,8 @@ def test_llm_math_chain(client: TestClient):
)
-def test_series_character_chain(client: TestClient):
- response = client.get("api/v1/all")
+def test_series_character_chain(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
chains = json_response["chains"]
@@ -367,8 +367,8 @@ def test_series_character_chain(client: TestClient):
)
-def test_mid_journey_prompt_chain(client: TestClient):
- response = client.get("api/v1/all")
+def test_mid_journey_prompt_chain(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
chains = json_response["chains"]
@@ -408,8 +408,8 @@ def test_mid_journey_prompt_chain(client: TestClient):
)
-def test_time_travel_guide_chain(client: TestClient):
- response = client.get("api/v1/all")
+def test_time_travel_guide_chain(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
chains = json_response["chains"]
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 408500d7a..4ed00893e 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -23,8 +23,14 @@ def test_components_path(runner, client, default_settings):
result = runner.invoke(
app,
- ["--components-path", str(temp_dir), *default_settings],
+ ["run", "--components-path", str(temp_dir), *default_settings],
)
assert result.exit_code == 0, result.stdout
settings_manager = utils.get_settings_manager()
- assert temp_dir in settings_manager.settings.COMPONENTS_PATH
+ assert str(temp_dir) in settings_manager.settings.COMPONENTS_PATH
+
+
+def test_superuser(runner, client, session):
+ result = runner.invoke(app, ["superuser"], input="admin\nadmin\n")
+ assert result.exit_code == 0, result.stdout
+ assert "Superuser created successfully." in result.stdout
diff --git a/tests/test_custom_component.py b/tests/test_custom_component.py
index 4dc8c9f1a..e75dc0e5b 100644
--- a/tests/test_custom_component.py
+++ b/tests/test_custom_component.py
@@ -473,15 +473,16 @@ def test_build_config_no_code():
@pytest.fixture
-def component():
+def component(client, active_user):
return CustomComponent(
+ user_id=active_user.id,
field_config={
"fields": {
"llm": {"type": "str"},
"url": {"type": "str"},
"year": {"type": "int"},
}
- }
+ },
)
diff --git a/tests/test_database.py b/tests/test_database.py
index 52a5daa4c..e4f68ca56 100644
--- a/tests/test_database.py
+++ b/tests/test_database.py
@@ -1,8 +1,9 @@
-import json
+from langflow.services.database.models.base import orjson_dumps
+import orjson
import pytest
from uuid import UUID, uuid4
-from sqlalchemy.orm import Session
+from sqlmodel import Session
from fastapi.testclient import TestClient
@@ -16,7 +17,7 @@ def json_style():
# color: str = Field(index=True)
# emoji: str = Field(index=False)
# flow_id: UUID = Field(default=None, foreign_key="flow.id")
- return json.dumps(
+ return orjson_dumps(
{
"color": "red",
"emoji": "👍",
@@ -24,63 +25,69 @@ def json_style():
)
-def test_create_flow(client: TestClient, json_flow: str):
- flow = json.loads(json_flow)
+def test_create_flow(
+ client: TestClient, json_flow: str, active_user, logged_in_headers
+):
+ flow = orjson.loads(json_flow)
data = flow["data"]
flow = FlowCreate(name="Test Flow", description="description", data=data)
- response = client.post("api/v1/flows/", json=flow.dict())
+ response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
# flow is optional so we can create a flow without a flow
flow = FlowCreate(name="Test Flow")
- response = client.post("api/v1/flows/", json=flow.dict(exclude_unset=True))
+ response = client.post(
+ "api/v1/flows/", json=flow.dict(exclude_unset=True), headers=logged_in_headers
+ )
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
-def test_read_flows(client: TestClient, json_flow: str):
- flow_data = json.loads(json_flow)
+def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_headers):
+ flow_data = orjson.loads(json_flow)
data = flow_data["data"]
flow = FlowCreate(name="Test Flow", description="description", data=data)
- response = client.post("api/v1/flows/", json=flow.dict())
+ response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
flow = FlowCreate(name="Test Flow", description="description", data=data)
- response = client.post("api/v1/flows/", json=flow.dict())
+ response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
- response = client.get("api/v1/flows/")
+ response = client.get("api/v1/flows/", headers=logged_in_headers)
assert response.status_code == 200
assert len(response.json()) > 0
-def test_read_flow(client: TestClient, json_flow: str):
- flow = json.loads(json_flow)
+def test_read_flow(client: TestClient, json_flow: str, active_user, logged_in_headers):
+ flow = orjson.loads(json_flow)
data = flow["data"]
flow = FlowCreate(name="Test Flow", description="description", data=data)
- response = client.post("api/v1/flows/", json=flow.dict())
+ response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
flow_id = response.json()["id"] # flow_id should be a UUID but is a string
# turn it into a UUID
flow_id = UUID(flow_id)
- response = client.get(f"api/v1/flows/{flow_id}")
+ response = client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
-def test_update_flow(client: TestClient, json_flow: str):
- flow = json.loads(json_flow)
+def test_update_flow(
+ client: TestClient, json_flow: str, active_user, logged_in_headers
+):
+ flow = orjson.loads(json_flow)
data = flow["data"]
flow = FlowCreate(name="Test Flow", description="description", data=data)
- response = client.post("api/v1/flows/", json=flow.dict())
+ response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
flow_id = response.json()["id"]
updated_flow = FlowUpdate(
@@ -88,7 +95,9 @@ def test_update_flow(client: TestClient, json_flow: str):
description="updated description",
data=data,
)
- response = client.patch(f"api/v1/flows/{flow_id}", json=updated_flow.dict())
+ response = client.patch(
+ f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers
+ )
assert response.status_code == 200
assert response.json()["name"] == updated_flow.name
@@ -96,19 +105,23 @@ def test_update_flow(client: TestClient, json_flow: str):
# assert response.json()["data"] == updated_flow.data
-def test_delete_flow(client: TestClient, json_flow: str):
- flow = json.loads(json_flow)
+def test_delete_flow(
+ client: TestClient, json_flow: str, active_user, logged_in_headers
+):
+ flow = orjson.loads(json_flow)
data = flow["data"]
flow = FlowCreate(name="Test Flow", description="description", data=data)
- response = client.post("api/v1/flows/", json=flow.dict())
+ response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
flow_id = response.json()["id"]
- response = client.delete(f"api/v1/flows/{flow_id}")
+ response = client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["message"] == "Flow deleted successfully"
-def test_create_flows(client: TestClient, session: Session, json_flow: str):
- flow = json.loads(json_flow)
+def test_create_flows(
+ client: TestClient, session: Session, json_flow: str, logged_in_headers
+):
+ flow = orjson.loads(json_flow)
data = flow["data"]
# Create test data
flow_list = FlowListCreate(
@@ -118,7 +131,9 @@ def test_create_flows(client: TestClient, session: Session, json_flow: str):
]
)
# Make request to endpoint
- response = client.post("api/v1/flows/batch/", json=flow_list.dict())
+ response = client.post(
+ "api/v1/flows/batch/", json=flow_list.dict(), headers=logged_in_headers
+ )
# Check response status code
assert response.status_code == 201
# Check response data
@@ -132,8 +147,10 @@ def test_create_flows(client: TestClient, session: Session, json_flow: str):
assert response_data[1]["data"] == data
-def test_upload_file(client: TestClient, session: Session, json_flow: str):
- flow = json.loads(json_flow)
+def test_upload_file(
+ client: TestClient, session: Session, json_flow: str, logged_in_headers
+):
+ flow = orjson.loads(json_flow)
data = flow["data"]
# Create test data
flow_list = FlowListCreate(
@@ -142,10 +159,11 @@ def test_upload_file(client: TestClient, session: Session, json_flow: str):
FlowCreate(name="Flow 2", description="description", data=data),
]
)
- file_contents = json.dumps(flow_list.dict())
+ file_contents = orjson_dumps(flow_list.dict())
response = client.post(
"api/v1/flows/upload/",
files={"file": ("examples.json", file_contents, "application/json")},
+ headers=logged_in_headers,
)
# Check response status code
assert response.status_code == 201
@@ -160,8 +178,10 @@ def test_upload_file(client: TestClient, session: Session, json_flow: str):
assert response_data[1]["data"] == data
-def test_download_file(client: TestClient, session: Session, json_flow):
- flow = json.loads(json_flow)
+def test_download_file(
+ client: TestClient, session: Session, json_flow, active_user, logged_in_headers
+):
+ flow = orjson.loads(json_flow)
data = flow["data"]
# Create test data
flow_list = FlowListCreate(
@@ -171,11 +191,12 @@ def test_download_file(client: TestClient, session: Session, json_flow):
]
)
for flow in flow_list.flows:
+ flow.user_id = active_user.id
db_flow = Flow.from_orm(flow)
session.add(db_flow)
session.commit()
# Make request to endpoint
- response = client.get("api/v1/flows/download/")
+ response = client.get("api/v1/flows/download/", headers=logged_in_headers)
# Check response status code
assert response.status_code == 200
# Check response data
@@ -189,32 +210,44 @@ def test_download_file(client: TestClient, session: Session, json_flow):
assert response_data[1]["data"] == data
-def test_create_flow_with_invalid_data(client: TestClient):
+def test_create_flow_with_invalid_data(
+ client: TestClient, active_user, logged_in_headers
+):
flow = {"name": "a" * 256, "data": "Invalid flow data"}
- response = client.post("api/v1/flows/", json=flow)
+ response = client.post("api/v1/flows/", json=flow, headers=logged_in_headers)
assert response.status_code == 422
-def test_get_nonexistent_flow(client: TestClient):
+def test_get_nonexistent_flow(client: TestClient, active_user, logged_in_headers):
uuid = uuid4()
- response = client.get(f"api/v1/flows/{uuid}")
+ response = client.get(f"api/v1/flows/{uuid}", headers=logged_in_headers)
assert response.status_code == 404
-def test_update_flow_idempotency(client: TestClient, json_flow: str):
- flow_data = json.loads(json_flow)
+def test_update_flow_idempotency(
+ client: TestClient, json_flow: str, active_user, logged_in_headers
+):
+ flow_data = orjson.loads(json_flow)
data = flow_data["data"]
flow_data = FlowCreate(name="Test Flow", description="description", data=data)
- response = client.post("api/v1/flows/", json=flow_data.dict())
+ response = client.post(
+ "api/v1/flows/", json=flow_data.dict(), headers=logged_in_headers
+ )
flow_id = response.json()["id"]
updated_flow = FlowCreate(name="Updated Flow", description="description", data=data)
- response1 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.dict())
- response2 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.dict())
+ response1 = client.put(
+ f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers
+ )
+ response2 = client.put(
+ f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers
+ )
assert response1.json() == response2.json()
-def test_update_nonexistent_flow(client: TestClient, json_flow: str):
- flow_data = json.loads(json_flow)
+def test_update_nonexistent_flow(
+ client: TestClient, json_flow: str, active_user, logged_in_headers
+):
+ flow_data = orjson.loads(json_flow)
data = flow_data["data"]
uuid = uuid4()
updated_flow = FlowCreate(
@@ -222,17 +255,19 @@ def test_update_nonexistent_flow(client: TestClient, json_flow: str):
description="description",
data=data,
)
- response = client.patch(f"api/v1/flows/{uuid}", json=updated_flow.dict())
+ response = client.patch(
+ f"api/v1/flows/{uuid}", json=updated_flow.dict(), headers=logged_in_headers
+ )
assert response.status_code == 404
-def test_delete_nonexistent_flow(client: TestClient):
+def test_delete_nonexistent_flow(client: TestClient, active_user, logged_in_headers):
uuid = uuid4()
- response = client.delete(f"api/v1/flows/{uuid}")
+ response = client.delete(f"api/v1/flows/{uuid}", headers=logged_in_headers)
assert response.status_code == 404
-def test_read_empty_flows(client: TestClient):
- response = client.get("api/v1/flows/")
+def test_read_empty_flows(client: TestClient, active_user, logged_in_headers):
+ response = client.get("api/v1/flows/", headers=logged_in_headers)
assert response.status_code == 200
assert len(response.json()) == 0
diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py
index 045af1ba5..cbb1eb08c 100644
--- a/tests/test_endpoints.py
+++ b/tests/test_endpoints.py
@@ -1,3 +1,7 @@
+import uuid
+from langflow.services.auth.utils import get_password_hash
+from langflow.services.database.models.api_key.api_key import ApiKey
+from langflow.services.utils import get_settings_manager
import pytest
from fastapi.testclient import TestClient
from langflow.interface.tools.constants import CUSTOM_TOOLS
@@ -83,8 +87,141 @@ PROMPT_REQUEST = {
}
-def test_get_all(client: TestClient):
- response = client.get("api/v1/all")
+@pytest.fixture
+def created_api_key(session, active_user):
+ hashed = get_password_hash("random_key")
+ api_key = ApiKey(
+ name="test_api_key",
+ user_id=active_user.id,
+ api_key="random_key",
+ hashed_api_key=hashed,
+ )
+
+ session.add(api_key)
+ session.commit()
+ session.refresh(api_key)
+ return api_key
+
+
+def test_process_flow_invalid_api_key(client, flow, monkeypatch):
+ # Mock de process_graph_cached
+ def mock_process_graph_cached(*args, **kwargs):
+ return {}, "session_id_mock"
+
+ settings_manager = get_settings_manager()
+ settings_manager.auth_settings.AUTO_LOGIN = False
+ from langflow.api.v1 import endpoints
+
+ monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
+
+ headers = {"api-key": "invalid_api_key"}
+
+ post_data = {
+ "inputs": {"key": "value"},
+ "tweaks": None,
+ "clear_cache": False,
+ "session_id": None,
+ }
+
+ response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data)
+
+ assert response.status_code == 403
+ assert response.json() == {"detail": "Invalid or missing API key"}
+
+
+def test_process_flow_invalid_id(client, monkeypatch, created_api_key):
+ def mock_process_graph_cached(*args, **kwargs):
+ return {}, "session_id_mock"
+
+ from langflow.api.v1 import endpoints
+
+ monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
+
+ api_key = created_api_key.api_key
+ headers = {"api-key": api_key}
+
+ post_data = {
+ "inputs": {"key": "value"},
+ "tweaks": None,
+ "clear_cache": False,
+ "session_id": None,
+ }
+
+ invalid_id = uuid.uuid4()
+ response = client.post(
+ f"api/v1/process/{invalid_id}", headers=headers, json=post_data
+ )
+
+ assert response.status_code == 404
+ assert f"Flow {invalid_id} not found" in response.json()["detail"]
+
+
+def test_process_flow_without_autologin(client, flow, monkeypatch, created_api_key):
+ # Mock de process_graph_cached
+ from langflow.api.v1 import endpoints
+
+ settings_manager = get_settings_manager()
+ settings_manager.auth_settings.AUTO_LOGIN = False
+
+ def mock_process_graph_cached(*args, **kwargs):
+ return {}, "session_id_mock"
+
+ monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
+
+ api_key = created_api_key.api_key
+ headers = {"api-key": api_key}
+
+ # Dummy POST data
+ post_data = {
+ "inputs": {"key": "value"},
+ "tweaks": None,
+ "clear_cache": False,
+ "session_id": None,
+ }
+
+ # Make the request to the FastAPI TestClient
+
+ response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data)
+
+ # Check the response
+ assert response.status_code == 200, response.json()
+ assert response.json()["result"] == {}
+ assert response.json()["session_id"] == "session_id_mock"
+
+
+def test_process_flow_fails_autologin_off(client, flow, monkeypatch):
+ # Mock de process_graph_cached
+ from langflow.api.v1 import endpoints
+
+ settings_manager = get_settings_manager()
+ settings_manager.auth_settings.AUTO_LOGIN = False
+
+ def mock_process_graph_cached(*args, **kwargs):
+ return {}, "session_id_mock"
+
+ monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
+
+ headers = {"api-key": "api_key"}
+
+ # Dummy POST data
+ post_data = {
+ "inputs": {"key": "value"},
+ "tweaks": None,
+ "clear_cache": False,
+ "session_id": None,
+ }
+
+ # Make the request to the FastAPI TestClient
+
+ response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data)
+
+ # Check the response
+ assert response.status_code == 403, response.json()
+ assert response.json() == {"detail": "Invalid or missing API key"}
+
+
+def test_get_all(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
# We need to test the custom nodes
diff --git a/tests/test_llms_template.py b/tests/test_llms_template.py
index f1b76e18e..14e151479 100644
--- a/tests/test_llms_template.py
+++ b/tests/test_llms_template.py
@@ -2,9 +2,9 @@ from fastapi.testclient import TestClient
from langflow.services.utils import get_settings_manager
-def test_llms_settings(client: TestClient):
+def test_llms_settings(client: TestClient, logged_in_headers):
settings_manager = get_settings_manager()
- response = client.get("api/v1/all")
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
llms = json_response["llms"]
@@ -103,8 +103,8 @@ def test_llms_settings(client: TestClient):
# }
-def test_openai(client: TestClient):
- response = client.get("api/v1/all")
+def test_openai(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
language_models = json_response["llms"]
@@ -369,8 +369,8 @@ def test_openai(client: TestClient):
}
-def test_chat_open_ai(client: TestClient):
- response = client.get("api/v1/all")
+def test_chat_open_ai(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
language_models = json_response["llms"]
@@ -542,8 +542,7 @@ def test_chat_open_ai(client: TestClient):
}
assert template["_type"] == "ChatOpenAI"
assert (
- model["description"]
- == "Wrapper around OpenAI Chat large language models." # noqa E501
+ model["description"] == "`OpenAI` Chat large language models API." # noqa E501
)
assert set(model["base_classes"]) == {
"BaseLLM",
diff --git a/tests/test_loading.py b/tests/test_loading.py
index 11fa8e471..e5c409c93 100644
--- a/tests/test_loading.py
+++ b/tests/test_loading.py
@@ -1,5 +1,4 @@
import json
-
import pytest
from langchain.chains.base import Chain
from langflow.processing.process import load_flow_from_json
diff --git a/tests/test_login.py b/tests/test_login.py
new file mode 100644
index 000000000..07abb35ab
--- /dev/null
+++ b/tests/test_login.py
@@ -0,0 +1,47 @@
+import pytest
+from langflow.services.database.models.user import User
+from langflow.services.auth.utils import get_password_hash
+
+
+@pytest.fixture
+def test_user():
+ return User(
+ username="testuser",
+ password=get_password_hash(
+ "testpassword"
+ ), # Assuming password needs to be hashed
+ is_active=True,
+ is_superuser=False,
+ )
+
+
+def test_login_successful(client, test_user, session):
+ # Adding the test user to the database
+ session.add(test_user)
+ session.commit()
+
+ response = client.post(
+ "api/v1/login", data={"username": "testuser", "password": "testpassword"}
+ )
+ assert response.status_code == 200
+ assert "access_token" in response.json()
+
+
+def test_login_unsuccessful_wrong_username(client):
+ response = client.post(
+ "api/v1/login", data={"username": "wrongusername", "password": "testpassword"}
+ )
+ assert response.status_code == 401
+ assert response.json()["detail"] == "Incorrect username or password"
+
+
+def test_login_unsuccessful_wrong_password(client, test_user, session):
+ # Adding the test user to the database
+ session.add(test_user)
+ session.commit()
+
+ response = client.post(
+ "api/v1/login", data={"username": "testuser", "password": "wrongpassword"}
+ )
+ assert response.status_code == 401
+ assert response.json()["detail"] == "Incorrect username or password"
diff --git a/tests/test_prompts_template.py b/tests/test_prompts_template.py
index dde313c20..676448f73 100644
--- a/tests/test_prompts_template.py
+++ b/tests/test_prompts_template.py
@@ -2,17 +2,17 @@ from fastapi.testclient import TestClient
from langflow.services.utils import get_settings_manager
-def test_prompts_settings(client: TestClient):
+def test_prompts_settings(client: TestClient, logged_in_headers):
settings_manager = get_settings_manager()
- response = client.get("api/v1/all")
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
prompts = json_response["prompts"]
assert set(prompts.keys()) == set(settings_manager.settings.PROMPTS)
-def test_prompt_template(client: TestClient):
- response = client.get("api/v1/all")
+def test_prompt_template(client: TestClient, logged_in_headers):
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
prompts = json_response["prompts"]
diff --git a/tests/test_user.py b/tests/test_user.py
new file mode 100644
index 000000000..d734e4d61
--- /dev/null
+++ b/tests/test_user.py
@@ -0,0 +1,213 @@
+from datetime import datetime
+from langflow.services.auth.utils import create_super_user, get_password_hash
+
+from langflow.services.database.models.user.user import User
+from langflow.services.utils import get_settings_manager
+import pytest
+from langflow.services.database.models.user import UserUpdate
+
+
+@pytest.fixture
+def super_user(client, session):
+ return create_super_user(session)
+
+
+@pytest.fixture
+def super_user_headers(client, super_user):
+ settings_manager = get_settings_manager()
+ auth_settings = settings_manager.auth_settings
+ login_data = {
+ "username": auth_settings.FIRST_SUPERUSER,
+ "password": auth_settings.FIRST_SUPERUSER_PASSWORD,
+ }
+ response = client.post("/api/v1/login", data=login_data)
+ assert response.status_code == 200
+ tokens = response.json()
+ a_token = tokens["access_token"]
+ return {"Authorization": f"Bearer {a_token}"}
+
+
+@pytest.fixture
+def deactivated_user(session):
+ user = User(
+ username="deactivateduser",
+ password=get_password_hash("testpassword"),
+ is_active=False,
+ is_superuser=False,
+ last_login_at=datetime.now(),
+ )
+ session.add(user)
+ session.commit()
+ return user
+
+
+def test_user_waiting_for_approval(client, session):
+ # Create a user that is not active and has never logged in
+ user = User(
+ username="waitingforapproval",
+ password=get_password_hash("testpassword"),
+ is_active=False,
+ last_login_at=None,
+ )
+ session.add(user)
+ session.commit()
+
+ login_data = {"username": "waitingforapproval", "password": "testpassword"}
+ response = client.post("/api/v1/login", data=login_data)
+ assert response.status_code == 400
+ assert response.json()["detail"] == "Waiting for approval"
+
+
+def test_deactivated_user_cannot_login(client, deactivated_user):
+ login_data = {"username": deactivated_user.username, "password": "testpassword"}
+ response = client.post("/api/v1/login", data=login_data)
+ assert response.status_code == 400, response.json()
+ assert response.json()["detail"] == "Inactive user"
+
+
+def test_deactivated_user_cannot_access(client, deactivated_user, logged_in_headers):
+ # Assuming the headers for deactivated_user
+ response = client.get("/api/v1/users", headers=logged_in_headers)
+ assert response.status_code == 400, response.json()
+ assert response.json()["detail"] == "The user doesn't have enough privileges"
+
+
+def test_data_consistency_after_update(client, active_user, logged_in_headers):
+ user_id = active_user.id
+ update_data = UserUpdate(username="newname")
+
+ response = client.patch(
+ f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
+ )
+ assert response.status_code == 200
+
+ # Fetch the updated user from the database
+ response = client.get("/api/v1/user", headers=logged_in_headers)
+ assert response.json()["username"] == "newname", response.json()
+
+
+def test_data_consistency_after_delete(client, test_user, super_user_headers):
+ user_id = test_user["id"]
+ response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
+ assert response.status_code == 200
+
+ # Attempt to fetch the deleted user from the database
+ response = client.get("/api/v1/users", headers=super_user_headers)
+ assert response.status_code == 200
+ assert all(user["id"] != user_id for user in response.json()["users"])
+
+
+def test_inactive_user(client, session):
+ # Create a user that is not active and has a last_login_at value
+ user = User(
+ username="inactiveuser",
+ password=get_password_hash("testpassword"),
+ is_active=False,
+ last_login_at="2023-01-01T00:00:00", # Set to a valid datetime string
+ )
+ session.add(user)
+ session.commit()
+
+ login_data = {"username": "inactiveuser", "password": "testpassword"}
+ response = client.post("/api/v1/login", data=login_data)
+ assert response.status_code == 400
+ assert response.json()["detail"] == "Inactive user"
+
+
+def test_add_user(client, test_user):
+ assert test_user["username"] == "testuser"
+
+
+# This is not used in the Frontend at the moment
+# def test_read_current_user(client: TestClient, active_user):
+# # First we need to login to get the access token
+# login_data = {"username": "testuser", "password": "testpassword"}
+# response = client.post("/api/v1/login", data=login_data)
+# assert response.status_code == 200
+
+# headers = {"Authorization": f"Bearer {response.json()['access_token']}"}
+
+# response = client.get("/api/v1/user", headers=headers)
+# assert response.status_code == 200, response.json()
+# assert response.json()["username"] == "testuser"
+
+
+def test_read_all_users(client, super_user_headers):
+ response = client.get("/api/v1/users", headers=super_user_headers)
+ assert response.status_code == 200, response.json()
+ assert isinstance(response.json()["users"], list)
+
+
+def test_normal_user_cant_read_all_users(client, logged_in_headers):
+ response = client.get("/api/v1/users", headers=logged_in_headers)
+ assert response.status_code == 400, response.json()
+ assert response.json() == {"detail": "The user doesn't have enough privileges"}
+
+
+def test_patch_user(client, active_user, logged_in_headers):
+ user_id = active_user.id
+ update_data = UserUpdate(
+ username="newname",
+ )
+
+ response = client.patch(
+ f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
+ )
+ assert response.status_code == 200, response.json()
+
+
+def test_patch_user_wrong_id(client, active_user, logged_in_headers):
+ user_id = "wrong_id"
+ update_data = UserUpdate(
+ username="newname",
+ )
+
+ response = client.patch(
+ f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
+ )
+ assert response.status_code == 422, response.json()
+ assert response.json() == {
+ "detail": [
+ {
+ "loc": ["path", "user_id"],
+ "msg": "value is not a valid uuid",
+ "type": "type_error.uuid",
+ }
+ ]
+ }
+
+
+def test_delete_user(client, test_user, super_user_headers):
+ user_id = test_user["id"]
+ response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
+ assert response.status_code == 200
+ assert response.json() == {"detail": "User deleted"}
+
+
+def test_delete_user_wrong_id(client, test_user, super_user_headers):
+ user_id = "wrong_id"
+ response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "loc": ["path", "user_id"],
+ "msg": "value is not a valid uuid",
+ "type": "type_error.uuid",
+ }
+ ]
+ }
+
+
+def test_normal_user_cant_delete_user(client, test_user, logged_in_headers):
+ user_id = test_user["id"]
+ response = client.delete(f"/api/v1/user/{user_id}", headers=logged_in_headers)
+ assert response.status_code == 400
+ assert response.json() == {"detail": "The user doesn't have enough privileges"}
+
+
+# If you still want to test the superuser endpoint
+def test_add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(client):
+ response = client.post("/api/v1/super_user")
+ assert response.status_code == 200
+ assert response.json()["username"] == "superuser"
diff --git a/tests/test_vectorstore_template.py b/tests/test_vectorstore_template.py
index 4baa7f4b6..87394b890 100644
--- a/tests/test_vectorstore_template.py
+++ b/tests/test_vectorstore_template.py
@@ -4,9 +4,9 @@ from langflow.services.utils import get_settings_manager
# check that all agents are in settings.agents
# are in json_response["agents"]
-def test_vectorstores_settings(client: TestClient):
+def test_vectorstores_settings(client: TestClient, logged_in_headers):
settings_manager = get_settings_manager()
- response = client.get("api/v1/all")
+ response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
vectorstores = json_response["vectorstores"]
diff --git a/tests/test_websocket.py b/tests/test_websocket.py
index dd668c287..16f9eff05 100644
--- a/tests/test_websocket.py
+++ b/tests/test_websocket.py
@@ -1,13 +1,16 @@
from fastapi import WebSocketDisconnect
+from fastapi.testclient import TestClient
# from langflow.services.chat.manager import ChatManager
import pytest
-def test_init_build(client):
+def test_init_build(client, active_user, logged_in_headers):
response = client.post(
- "api/v1/build/init/test", json={"id": "test", "data": {"key": "value"}}
+ "api/v1/build/init/test",
+ json={"id": "test", "data": {"key": "value"}},
+ headers=logged_in_headers,
)
assert response.status_code == 201
assert response.json() == {"flowId": "test"}
@@ -24,10 +27,12 @@ def test_init_build(client):
# assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
-def test_websocket_endpoint(client):
+def test_websocket_endpoint(client: TestClient, active_user, logged_in_headers):
+ # Assuming your websocket_endpoint uses chat_manager which caches data from stream_build
+ access_token = logged_in_headers["Authorization"].split(" ")[1]
with pytest.raises(WebSocketDisconnect):
with client.websocket_connect(
- "api/v1/chat/non_existing_client_id"
+ f"api/v1/chat/non_existing_client_id?token={access_token}"
) as websocket:
websocket.send_json({"type": "test"})
data = websocket.receive_json()