### Description
This pull request introduces a significant enhancement to the project by
incorporating robust authentication logic. The enhancement addresses the
critical need for secure user access and data protection within the
application. With this addition, the system will be able to authenticate
users, verify their identities, and provide access only to authorized
individuals.

### Changes Implemented
- **Authentication Middleware**: A new middleware module has been
implemented to intercept incoming requests and validate user
authentication. This middleware checks for valid authentication tokens
or credentials and ensures that only authenticated users can access
protected resources.

- **User Authentication Endpoint**: A new endpoint has been created to
handle user authentication. This endpoint allows users to provide their
credentials, which are then securely processed and validated against the
stored user data.

- **Password Hashing**: User passwords are now securely hashed using a
strong cryptographic algorithm before being stored in the database. This
ensures that even if the database is compromised, user passwords remain
inaccessible.

- **Token-Based Authentication**: Upon successful authentication, the
system generates a time-limited access token for the user. This token
must be included in subsequent requests to access protected resources.
Token validity is maintained through token expiration and refresh
mechanisms.

- **Authorization Checks**: The logic has been extended to include
authorization checks after successful authentication. This ensures that
authenticated users only have access to the resources they are
authorized to use, based on their roles and permissions.

### Benefits
1. **Enhanced Security**: By implementing strong authentication
mechanisms and password hashing, the project significantly improves
security, minimizing the risk of unauthorized access or data breaches.

2. **User-Friendly Experience**: Users can securely access the
application, confident in the knowledge that their credentials and data
are protected. The token-based authentication simplifies the user
experience by eliminating the need for frequent re-authentication.

3. **Scalability**: The authentication logic has been designed with
scalability in mind, ensuring that the system can handle a growing
number of users and requests without compromising security or
performance.

4. **Modularity**: The new authentication logic has been integrated as a
separate module, enhancing the project's modularity and maintainability.
This commit is contained in:
anovazzi1 2023-08-29 17:38:20 -03:00 committed by GitHub
commit 02cc23d6b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
110 changed files with 4566 additions and 1804 deletions

View file

@ -1,6 +1,6 @@
import _ from "lodash";
import { useContext, useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import "reactflow/dist/style.css";
import "./App.css";
@ -42,8 +42,11 @@ export default function App() {
successData,
successOpen,
setSuccessOpen,
setErrorData,
loading,
setLoading,
} = useContext(alertContext);
const navigate = useNavigate();
const { fetchError } = useContext(typesContext);
// Initialize state variable for the list of alerts

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import {
Select,
SelectContent,
@ -12,17 +12,21 @@ import { Button } from "../ui/button";
export default function PaginatorComponent({
pageSize = 10,
pageIndex = 1,
rowsCount = [10, 20, 30],
pageIndex = 0,
rowsCount = [10, 20, 50, 100],
totalRowsCount = 0,
paginate,
}: PaginatorComponentType) {
const [size, setPageSize] = useState(pageSize);
const [index, setPageIndex] = useState(pageIndex);
const [maxIndex, setMaxPageIndex] = useState(
Math.ceil(totalRowsCount / pageSize)
);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
setMaxPageIndex(Math.ceil(totalRowsCount / size));
}, [totalRowsCount]);
return (
<>
@ -35,7 +39,7 @@ export default function PaginatorComponent({
onValueChange={(pageSize: string) => {
setPageSize(Number(pageSize));
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
paginate(Number(pageSize), index);
paginate(Number(pageSize), 0);
}}
>
<SelectTrigger className="w-[100px]">
@ -51,30 +55,30 @@ export default function PaginatorComponent({
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {index} of {maxIndex}
Page {currentPage} of {maxIndex}
</div>
<div className="flex items-center space-x-2">
<Button
disabled={index <= 0}
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
setPageIndex(1);
paginate(size, 1);
setPageIndex(0);
setCurrentPage(1);
paginate(size, 0);
}}
>
<span className="sr-only">Go to first page</span>
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
</Button>
<Button
disabled={index <= 0}
onClick={() => {
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({
<IconComponent name="ChevronLeft" className="h-4 w-4" />
</Button>
<Button
disabled={currentPage === maxIndex}
onClick={() => {
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({
<IconComponent name="ChevronRight" className="h-4 w-4" />
</Button>
<Button
disabled={currentPage === maxIndex}
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
setPageIndex(maxIndex);
paginate(size, maxIndex);
setPageIndex(maxIndex - 1);
setCurrentPage(maxIndex);
paginate(size, size);
}}
>
<span className="sr-only">Go to last page</span>

View file

@ -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 <Navigate to="/login" replace />;
}
if ((userData && !isAdmin) || autoLogin) {
return <Navigate to="/" replace />;
}
return children;
};

View file

@ -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 <Navigate to="/login" replace />;
}
return children;
};

View file

@ -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 <Navigate to="/" replace />;
}
if (getAuthentication()) {
window.location.replace("/");
return <Navigate to="/" replace />;
}
return children;
};

View file

@ -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;
};

View file

@ -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 (
<TargetIcon
className={className}
style={{ color: iconColor }}
stroke-width={1.5}
/>
);
}
const ForwardedIconComponent = forwardRef(
({ name, className, iconColor }: IconComponentProps, ref) => {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
return (
<TargetIcon
strokeWidth={1.5}
className={className}
style={iconColor ? { color: iconColor } : {}}
ref={ref}
/>
);
}
);
export default ForwardedIconComponent;

View file

@ -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 (
<div className="header-arrangement">
<div className="header-start-display">
<Link to="/">
<span className="ml-4 text-2xl"></span>
</Link>
<Button variant="outline" className="">
Sign out
</Button>
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (
<MenuBar flows={flows} tabId={tabId} />
)}
{!autoLogin && location.pathname !== `/flow/${tabId}` && (
<a
onClick={() => {
logout();
navigate("/login");
}}
className="text-sm font-medium text-muted-foreground transition-colors hover:text-primary cursor-pointer mx-5"
>
Sign out
</a>
)}
{location.pathname === "/admin" && (
<a
onClick={() => {
navigate("/");
}}
className="text-sm font-medium text-muted-foreground transition-colors hover:text-primary cursor-pointer"
>
Home
</a>
)}
{isAdmin &&
!autoLogin &&
location.pathname !== "/admin" &&
location.pathname !== `/flow/${tabId}` && (
<a
className="text-sm font-medium text-muted-foreground transition-colors hover:text-primary cursor-pointer"
onClick={() => navigate("/admin")}
>
Admin page
</a>
)}
</div>
<div className="round-button-div">
<Link to="/">
@ -119,6 +144,18 @@ export default function Header(): JSX.Element {
/>
</div>
</AlertDropdown>
{!autoLogin && (
<button
onClick={() => {
navigate("/account/api-keys");
}}
>
<IconComponent
name="Key"
className="side-bar-button-size text-muted-foreground hover:text-accent-foreground"
/>
</button>
)}
</div>
</div>
</div>

View file

@ -81,7 +81,8 @@ export default function InputComponent({
? "input-component-true-button"
: "input-component-false-button"
)}
onClick={() => {
onClick={(event) => {
event.preventDefault();
setPwdVisible(!pwdVisible);
}}
>

View file

@ -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) {
@ -605,3 +618,21 @@ 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.";

View file

@ -1,74 +1,120 @@
import { createContext, useEffect, useState } from "react";
import { AuthContextType, userData } from "../types/contexts/auth";
import { createContext, useContext, useEffect, useState } from "react";
import Cookies from "universal-cookie";
import { autoLogin as autoLoginApi, getLoggedUser } from "../controllers/API";
import { Users } from "../types/api";
import { AuthContextType } from "../types/contexts/auth";
import { alertContext } from "./alertContext";
const initialValue: AuthContextType = {
isAdmin: false,
setIsAdmin: () => 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<AuthContextType>(initialValue);
export const AuthContext = createContext<AuthContextType>(initialValue);
export function AuthProvider({ children }): React.ReactElement {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [userData, setUserData] = useState<userData | null>(null);
const cookies = new Cookies();
const [accessToken, setAccessToken] = useState<string | null>(
cookies.get("access_token")
);
const [refreshToken, setRefreshToken] = useState<string | null>(
cookies.get("refresh_token")
);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [isAdmin, setIsAdmin] = useState<boolean>(false);
const [userData, setUserData] = useState<Users | null>(null);
const [autoLogin, setAutoLogin] = useState<boolean>(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
<AuthContext.Provider
value={{
isAdmin,
setIsAdmin,
isAuthenticated: !!accessToken,
accessToken,
refreshToken,
login,
logout,
refreshAccessToken,
setUserData,
userData,
getAuthentication,
authenticationErrorCount: 0,
setAutoLogin,
autoLogin,
}}
>
{children}

View file

@ -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<darkContextType>(initialValue);
@ -12,6 +15,16 @@ export function DarkProvider({ children }) {
const [dark, setDark] = useState(
JSON.parse(window.localStorage.getItem("isDark")!) ?? false
);
const [stars, setStars] = useState<number>(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 (
<darkContext.Provider
value={{
setStars,
stars,
dark,
setDark,
}}

View file

@ -1,8 +1,11 @@
import { ReactNode } from "react";
import { BrowserRouter } from "react-router-dom";
import { ReactFlowProvider } from "reactflow";
import { TooltipProvider } from "../components/ui/tooltip";
import { ApiInterceptor } from "../controllers/API/api";
import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext";
import { AuthProvider } from "./authContext";
import { DarkProvider } from "./darkContext";
import { LocationProvider } from "./locationContext";
import { TabsProvider } from "./tabsContext";
@ -13,23 +16,28 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
//element to wrap all context
return (
<>
<TooltipProvider>
<ReactFlowProvider>
<DarkProvider>
<AlertProvider>
<TypesProvider>
<LocationProvider>
<SSEProvider>
<TabsProvider>
<UndoRedoProvider>{children}</UndoRedoProvider>
</TabsProvider>
</SSEProvider>
</LocationProvider>
</TypesProvider>
</AlertProvider>
</DarkProvider>
</ReactFlowProvider>
</TooltipProvider>
<BrowserRouter>
<AlertProvider>
<AuthProvider>
<TooltipProvider>
<ReactFlowProvider>
<DarkProvider>
<TypesProvider>
<LocationProvider>
<ApiInterceptor />
<SSEProvider>
<TabsProvider>
<UndoRedoProvider>{children}</UndoRedoProvider>
</TabsProvider>
</SSEProvider>
</LocationProvider>
</TypesProvider>
</DarkProvider>
</ReactFlowProvider>
</TooltipProvider>
</AuthProvider>
</AlertProvider>
</BrowserRouter>
</>
);
}

View file

@ -1,3 +1,4 @@
import { AxiosError } from "axios";
import _ from "lodash";
import {
ReactNode,
@ -21,7 +22,7 @@ import {
import { APIClassType, APITemplateType } from "../types/api";
import { tweakType } from "../types/components";
import { FlowType, NodeDataType, NodeType } from "../types/flow";
import { TabsContextType, TabsState, errorsVarType } from "../types/tabs";
import { TabsContextType, TabsState } from "../types/tabs";
import {
addVersionToDuplicates,
updateIds,
@ -29,6 +30,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 });
@ -68,7 +70,9 @@ export const TabsContext = createContext<TabsContextType>(
);
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("");
@ -117,24 +121,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 {
@ -143,9 +149,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
processFlowEdges(flow);
processFlowNodes(flow);
} catch (e) {
console.error(e);
}
} catch (e) {}
});
}
@ -478,7 +482,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 {
@ -579,6 +582,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);
@ -601,7 +605,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
}
} catch (err) {
setErrorData(err as errorsVarType);
setErrorData({
title: "Error while saving changes",
list: [(err as AxiosError).message],
});
}
}

View file

@ -10,6 +10,7 @@ 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
@ -37,55 +38,56 @@ export function TypesProvider({ children }: { children: ReactNode }) {
const [data, setData] = useState({});
const [fetchError, setFetchError] = useState(false);
const { setLoading } = useContext(alertContext);
const { getAuthentication } = useContext(AuthContext);
useEffect(() => {
// 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<void> {
// 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<void> {
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) => {
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;
}, {})
);
}
} catch (error) {
console.error("An error has occurred while fetching types.");
await getHealth().catch((e) => {
setFetchError(true);
});
);
}
} catch (error) {
console.error("An error has occurred while fetching types.");
await getHealth().catch((e) => {
setFetchError(true);
});
}
getTypes();
}, []);
}
function deleteNode(idx: string) {
reactFlowInstance!.setNodes(

View file

@ -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 };

View file

@ -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<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects.
*/
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
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<any>} 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<AxiosResponse<errorsTypeAPI>> {
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<AxiosResponse<PromptTypeAPI>> {
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<FlowType> {
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<FlowType> {
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<AxiosResponse<any>>} 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<BuildStatusTypeAPI> {
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<AxiosResponse<InitTypeAPI>> {
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<AxiosResponse<UploadFileTypeAPI>> {
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<AxiosResponse<APIClassType>> {
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<Users> {
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<Users> {
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;
}
}

View file

@ -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(
<ContextWrapper>
<BrowserRouter>
<App />
<ApiInterceptor />
</BrowserRouter>
<App />
</ContextWrapper>
);
reportWebVitals();

View file

@ -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<ApiKeyInputType>(CONTROL_NEW_API_KEY);
const [renderKey, setRenderKey] = useState(false);
const [textCopied, setTextCopied] = useState(true);
const { setSuccessData } = useContext(alertContext);
const inputRef = useRef<HTMLInputElement | null>(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 (
<BaseModal size="small-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={""}>
<span className="pr-2">{title}</span>
<Icon
name="icon"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
{renderKey === true && (
<>
<span className="text-xs">
Please save this secret key somewhere safe and accessible. For
security reasons,{" "}
<strong>you won't be able to view it again</strong> through your
account. If you lose this secret key, you'll need to generate a
new one.
</span>
<div className="flex pt-3">
<div className="w-full">
<Input
ref={inputRef}
onChange={(event) => {
setApiKeyValue(event.target.value);
}}
readOnly={true}
value={apiKeyValue}
/>
</div>
<div>
<Button
className="ml-3"
onClick={() => {
handleCopyClick();
}}
>
{textCopied ? (
<IconComponent name="Copy" className="h-4 w-4" />
) : (
<IconComponent name="Check" className="h-4 w-4" />
)}
</Button>
</div>
</div>
</>
)}
<Form.Root
onSubmit={(event) => {
setRenderKey(true);
handleAddNewKey();
event.preventDefault();
}}
>
{renderKey === false && (
<div className="grid gap-5">
<Form.Field name="username">
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
}}
>
<Form.Label className="data-[invalid]:label-invalid">
Name (optional){" "}
</Form.Label>
</div>
<Form.Control asChild>
<input
onChange={({ target: { value } }) => {
handleInput({ target: { name: "apikeyname", value } });
setApiKeyName(value);
}}
value={apiKeyName}
className="primary-input"
placeholder="My key name"
/>
</Form.Control>
</Form.Field>
</div>
)}
{renderKey === false && (
<div className="float-right">
<Button
className="mr-3"
variant="outline"
onClick={() => {
setOpen(false);
}}
>
{cancelText}
</Button>
<Form.Submit asChild>
<Button className="mt-8">{confirmationText}</Button>
</Form.Submit>
</div>
)}
{renderKey === true && (
<div className="float-right">
<Button
onClick={() => {
setOpen(false);
setRenderKey(false);
}}
className="mt-8"
>
Done
</Button>
</div>
)}
</Form.Root>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -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<UserInputType>(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({
</div>
<Form.Control asChild>
<input
onChange={(input) => {
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",
}}
>
<Form.Label className="data-[invalid]:label-invalid">
<Form.Label className="data-[invalid]:label-invalid flex">
Password{" "}
<span className="font-medium text-destructive">*</span>
<span className="ml-1 mr-1 font-medium text-destructive">
*
</span>
{pwdVisible && (
<Eye
onClick={() => setPwdVisible(!pwdVisible)}
className="h-5 cursor-pointer"
strokeWidth={1.5}
/>
)}
{!pwdVisible && (
<EyeOff
onClick={() => setPwdVisible(!pwdVisible)}
className="h-5 cursor-pointer"
strokeWidth={1.5}
/>
)}
</Form.Label>
</div>
<InputComponent
onChange={(input) => {
setPassword(input);
}}
value={password}
password={true}
isForm
className="primary-input"
required
placeholder="Password"
/>
<Form.Control asChild>
<input
onChange={({ target: { value } }) => {
handleInput({ target: { name: "password", value } });
setPassword(value);
}}
value={password}
className="primary-input"
required={data ? false : true}
type={pwdVisible ? "text" : "password"}
/>
</Form.Control>
<Form.Message className="field-invalid" match="valueMissing">
Please enter a password
</Form.Message>
@ -146,93 +184,108 @@ export default function UserManagementModal({
justifyContent: "space-between",
}}
>
<Form.Label className="data-[invalid]:label-invalid">
<Form.Label className="data-[invalid]:label-invalid flex">
Confirm password{" "}
<span className="font-medium text-destructive">*</span>
<span className="ml-1 mr-1 font-medium text-destructive">
*
</span>
{confirmPwdVisible && (
<Eye
onClick={() =>
setConfirmPwdVisible(!confirmPwdVisible)
}
className="h-5 cursor-pointer"
strokeWidth={1.5}
/>
)}
{!confirmPwdVisible && (
<EyeOff
onClick={() =>
setConfirmPwdVisible(!confirmPwdVisible)
}
className="h-5 cursor-pointer"
strokeWidth={1.5}
/>
)}
</Form.Label>
</div>
<InputComponent
onChange={(input) => {
setConfirmPassword(input);
}}
value={confirmPassword}
password={true}
isForm
className="primary-input"
required
placeholder="Confirm your password"
/>
<Form.Control asChild>
<input
onChange={(input) => {
setConfirmPassword(input.target.value);
}}
value={confirmPassword}
className="primary-input"
required={data ? false : true}
type={confirmPwdVisible ? "text" : "password"}
/>
</Form.Control>
<Form.Message className="field-invalid" match="valueMissing">
Please confirm your password
</Form.Message>
</Form.Field>
</div>
</div>
{/*
<Form.Field name="email">
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
}}
>
<Form.Label className="data-[invalid]:label-invalid">
Email <span className="font-medium text-destructive">*</span>
</Form.Label>
<Form.Message className="field-invalid" match="valueMissing">
Please enter your email
</Form.Message>
<Form.Message className="field-invalid" match="typeMismatch">
Please provide a valid email
</Form.Message>
</div>
<Form.Control asChild>
<input className="primary-input" type="email" required />
</Form.Control>
</Form.Field> */}
{/*
<Form.Field name="birth">
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
}}
>
<Form.Label className="data-[invalid]:label-invalid">
Date of birth{" "}
<span className="font-medium text-destructive">*</span>
</Form.Label>
<Form.Message className="field-invalid" match="valueMissing">
Please enter your date of birth
</Form.Message>
</div>
<Form.Control asChild>
<input
type="date"
className="primary-input"
required
max={new Date().toISOString().split("T")[0]}
/>
</Form.Control>
</Form.Field> */}
<div className="flex gap-8">
<Form.Field name="is_active">
<div>
<Form.Label className="data-[invalid]:label-invalid mr-3">
Active
</Form.Label>
<Form.Control asChild>
<Checkbox
value={isActive}
checked={isActive}
id="is_active"
className="relative top-0.5"
onCheckedChange={(value) => {
handleInput({ target: { name: "is_active", value } });
setIsActive(value);
}}
/>
</Form.Control>
</div>
</Form.Field>
{userData?.is_superuser && (
<Form.Field name="is_superuser">
<div>
<Form.Label className="data-[invalid]:label-invalid mr-3">
Superuser
</Form.Label>
<Form.Control asChild>
<Checkbox
checked={isSuperUser}
value={isSuperUser}
id="is_superuser"
className="relative top-0.5"
onCheckedChange={(value) => {
handleInput({
target: { name: "is_superuser", value },
});
setIsSuperUser(value);
}}
/>
</Form.Control>
</div>
</Form.Field>
)}
</div>
</div>
<div className="float-right">
<Form.Submit asChild>
<Button className="mr-3 mt-8">{confirmationText}</Button>
</Form.Submit>
<Button
variant="outline"
onClick={() => {
setOpen(false);
}}
className="mr-3"
>
{cancelText}
</Button>
<Form.Submit asChild>
<Button className="mt-8">{confirmationText}</Button>
</Form.Submit>
</div>
</Form.Root>
</BaseModal.Content>

View file

@ -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 (

View file

@ -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<ChatMessageType[]>([]);
const { reactFlowInstance } = useContext(typesContext);
const { accessToken } = useContext(AuthContext);
const { setErrorData } = useContext(alertContext);
const ws = useRef<WebSocket | null>(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) {

View file

@ -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",

View file

@ -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<loginInputStateType>(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() {
<div className="flex w-72 flex-col items-center justify-center gap-2">
<span className="mb-4 text-5xl"></span>
<span className="mb-6 text-2xl font-semibold text-primary">Admin</span>
<Input className="bg-background" placeholder="Email address" />
<Input className="bg-background" placeholder="Password" />
<Input
onChange={({ target: { value } }) => {
handleInput({ target: { name: "username", value } });
}}
className="bg-background"
placeholder="Username"
/>
<Input
type="password"
onChange={({ target: { value } }) => {
handleInput({ target: { name: "password", value } });
}}
className="bg-background"
placeholder="Password"
/>
<Button
onClick={() => {
loginAdmin();
signIn();
}}
variant="default"
className="w-full"

View file

@ -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 (
<>
<div className="main-page-panel">
<div className="m-auto flex h-full flex-row justify-center">
<div className="basis-5/6">
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">
Welcome back!
</h2>
<p className="text-muted-foreground">
Here&apos;s a list of all users!
</p>
</div>
<div className="flex items-center space-x-2"></div>
</div>
{userList.current.length === 0 && (
<>
<div className="flex items-center justify-between">
<h2>There's no users left :)</h2>
<div className="flex flex-col">
<Header />
{userData && (
<div className="main-page-panel">
<div className="m-auto flex h-full flex-row justify-center">
<div className="basis-5/6">
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">
Welcome back!
</h2>
<p className="text-muted-foreground">
Navigate through this section to efficiently oversee all
application users. From here, you can seamlessly manage
user accounts.
</p>
</div>
</>
)}
{userList.current.length > 0 && (
<div className="flex items-center space-x-2"></div>
</div>
{userList.current.length === 0 && !loadingUsers && (
<>
<div className="flex items-center justify-between">
<h2>There's no users registered :)</h2>
</div>
</>
)}
<>
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center space-x-2">
@ -285,8 +223,8 @@ export default function AdminPage() {
{inputValue.length > 0 && (
<Button
onClick={() => {
resetFilter();
setInputValue("");
setFilterUserList(userList.current);
}}
variant="ghost"
className="h-8 px-2 lg:px-3"
@ -311,90 +249,183 @@ export default function AdminPage() {
</UserManagementModal>
</div>
</div>
{loadingUsers && (
<div>
<strong>Loading...</strong>
</div>
)}
<div
className="overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted
custom-scroll"
className={
"max-h-[26rem] min-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
(loadingUsers ? " border-0" : "")
}
>
<Table className="table-fixed bg-muted outline-1 ">
<TableHeader>
<Table className={"table-fixed bg-muted outline-1"}>
<TableHeader
className={
loadingUsers
? "hidden"
: "table-fixed bg-muted outline-1"
}
>
<TableRow>
<TableHead className="h-10">User</TableHead>
<TableHead className="h-10">Password</TableHead>
<TableHead className="h-10">Id</TableHead>
<TableHead className="h-10">Username</TableHead>
<TableHead className="h-10">Active</TableHead>
<TableHead className="h-10">Superuser</TableHead>
<TableHead className="h-10">Created At</TableHead>
<TableHead className="h-10">Updated At</TableHead>
<TableHead className="h-10 w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filterUserList.map((user, index) => (
<TableRow key={user.user}>
<TableCell className="truncate py-2 font-medium">
{user.user}
</TableCell>
<TableCell className="truncate py-2">
{user.password}
</TableCell>
<TableCell className="flex w-[100px] py-2 text-right">
<div className="flex">
<UserManagementModal
title="Edit"
titleHeader={`${user.user}`}
cancelText="Cancel"
confirmationText="Edit"
icon={"UserPlus2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleEditUser(index, user);
}}
>
<ShadTooltip content="Edit" side="top">
<IconComponent
name="Pencil"
className="h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</UserManagementModal>
{!loadingUsers && (
<TableBody>
{filterUserList.map((user:UserInputType, index) => (
<TableRow key={index}>
<TableCell className="truncate py-2 font-medium">
<ShadTooltip content={user.id}>
<span className="cursor-default">
{user.id}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
<ShadTooltip content={user.username}>
<span className="cursor-default">
{user.username}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
<ConfirmationModal
title="Delete"
titleHeader="Delete User"
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this user? This action cannot be undone."
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDeleteUser(index);
handleDisableUser(
user.is_active,
user.id,
user
);
}}
>
<ShadTooltip content="Delete" side="top">
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
<Checkbox
id="is_active"
checked={user.is_active}
/>
</ConfirmationModal>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
<ConfirmationModal
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleSuperUserEdit(
user.is_superuser,
user.id,
user
);
}}
>
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</ConfirmationModal>
</TableCell>
<TableCell className="truncate py-2 ">
{
new Date(user.create_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="truncate py-2">
{
new Date(user.updated_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="flex w-[100px] py-2 text-right">
<div className="flex">
<UserManagementModal
title="Edit"
titleHeader={`${user.id}`}
cancelText="Cancel"
confirmationText="Save"
icon={"UserPlus2"}
data={user}
index={index}
onConfirm={(index, editUser) => {
handleEditUser(user.id, editUser);
}}
>
<ShadTooltip content="Edit" side="top">
<IconComponent
name="Pencil"
className="h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</UserManagementModal>
<ConfirmationModal
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this user? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDeleteUser(user);
}}
>
<ShadTooltip content="Delete" side="top">
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
)}
</Table>
</div>
<PaginatorComponent
pageIndex={index}
pageSize={size}
totalRowsCount={filterUserList.length}
totalRowsCount={totalRowsCount}
paginate={(pageIndex, pageSize) => {
handleChangePagination(pageSize, pageIndex);
}}
></PaginatorComponent>
</>
)}
</div>
</div>
</div>
</div>
)}
</div>
</>
);

View file

@ -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 (
<div className="text-xs">
<span>
{LAST_USED_SPAN_1}
<br></br> {LAST_USED_SPAN_2}
</span>
</div>
);
}
return (
<>
<Header></Header>
{userData && (
<div className="main-page-panel">
<div className="m-auto flex h-full flex-row justify-center">
<div className="basis-5/6">
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">
API keys
</h2>
<p className="text-muted-foreground">
{API_PAGE_PARAGRAPH_1}
<br />
{API_PAGE_PARAGRAPH_2}
</p>
</div>
<div className="flex items-center space-x-2"></div>
</div>
{keysList.current &&
keysList.current.length === 0 &&
!loadingKeys && (
<>
<div className="flex items-center justify-between">
<h2>{API_PAGE_USER_KEYS}</h2>
</div>
</>
)}
<>
{loadingKeys && (
<div>
<strong>Loading...</strong>
</div>
)}
<div
className={
"max-h-[15rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
(loadingKeys ? " border-0" : "")
}
>
{keysList.current &&
keysList.current.length > 0 &&
!loadingKeys && (
<Table className={"table-fixed bg-muted outline-1"}>
<TableHeader
className={
loadingKeys
? "hidden"
: "table-fixed bg-muted outline-1"
}
>
<TableRow>
<TableHead className="h-10">Name</TableHead>
<TableHead className="h-10">Key</TableHead>
<TableHead className="h-10">Created</TableHead>
<TableHead className="flex h-10 items-center">
Last Used
<ShadTooltip
side="top"
content={lastUsedMessage()}
>
<div>
<IconComponent
name="Info"
className="ml-1 h-3 w-3"
/>
</div>
</ShadTooltip>
</TableHead>
<TableHead className="h-10">Total Uses</TableHead>
<TableHead className="h-10 w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
{!loadingKeys && (
<TableBody>
{keysList.current.map(
(api_keys: ApiKey, index: number) => (
<TableRow key={index}>
<TableCell className="truncate py-2">
<ShadTooltip content={api_keys.name}>
<span className="cursor-default">
{api_keys.name ? api_keys.name : "-"}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
<span className="cursor-default">
{api_keys.api_key}
</span>
</TableCell>
<TableCell className="truncate py-2 ">
<ShadTooltip
side="top"
content={moment(
api_keys.created_at
).format("YYYY-MM-DD HH:mm")}
>
<div>
{moment(api_keys.created_at).format(
"YYYY-MM-DD HH:mm"
)}
</div>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
<ShadTooltip
side="top"
content={
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")
}
>
<div>
{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")}
</div>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
{api_keys.total_uses}
</TableCell>
<TableCell className="flex w-[100px] py-2 text-right">
<div className="flex">
<ConfirmationModal
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this key? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
data={api_keys.id}
index={index}
onConfirm={(index, keys) => {
handleDeleteKey(keys);
}}
>
<ShadTooltip
content="Delete"
side="top"
>
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal>
</div>
</TableCell>
</TableRow>
)
)}
</TableBody>
)}
</Table>
)}
</div>
<div className="flex items-center justify-between">
<div>
<SecretKeyModal
title="Create new secret key"
cancelText="Cancel"
confirmationText="Create secret key"
icon={"Key"}
data={userId}
onCloseModal={getKeys}
>
<Button>
<IconComponent name="Plus" className="mr-1 h-5 w-5" />
Create new secret key
</Button>
</SecretKeyModal>
</div>
</div>
</>
</div>
</div>
</div>
</div>
)}
</>
);
}

View file

@ -125,7 +125,6 @@ export default function ExtraSidebar(): JSX.Element {
}
onClick={(event) => {
saveFlow(flow!);
setSuccessData({ title: "Changes saved successfully" });
}}
>
<IconComponent

View file

@ -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<loginInputStateType>(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 (
<Form.Root
onSubmit={(event) => {
@ -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 {
</Form.Submit>
</div>
<div className="w-full">
<Link to="">
<Link to="/signup">
<Button className="w-full" variant="outline">
Don't have an account?&nbsp;<b>Sign Up</b>
</Button>

View file

@ -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<signUpInputStateType>(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 (
<Form.Root
onSubmit={(event: FormEvent<HTMLFormElement>) => {
@ -120,7 +156,14 @@ export default function SignUp(): JSX.Element {
</div>
<div className="w-full">
<Form.Submit asChild>
<Button className="mr-3 mt-6 w-full">Sign up</Button>
<Button
className="mr-3 mt-6 w-full"
onClick={() => {
handleSignup();
}}
>
Sign up
</Button>
</Form.Submit>
</div>
<div className="w-full">

View file

@ -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 (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/community" element={<CommunityPage />} />
<Route
path="/"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
/>
<Route
path="/community"
element={
<ProtectedRoute>
<CommunityPage />
</ProtectedRoute>
}
/>
<Route path="/flow/:id/">
<Route path="" element={<FlowPage />} />
<Route path="view" element={<ViewPage />} />
<Route
path=""
element={
<ProtectedRoute>
<FlowPage />
</ProtectedRoute>
}
/>
<Route
path="view"
element={
<ProtectedRoute>
<ViewPage />
</ProtectedRoute>
}
/>
</Route>
<Route path="*" element={<HomePage />} />
<Route
path="*"
element={
<ProtectedRoute>
<CatchAllRoute />
</ProtectedRoute>
}
/>
<Route path="/login" element={<LoginPage />} />
{/* <Route path="/signup" element={<SignUp />} /> */}
<Route path="/login/admin" element={<LoginAdminPage />} />
<Route
path="/login"
element={
<ProtectedLoginRoute>
<LoginPage />
</ProtectedLoginRoute>
}
/>
<Route
path="/signup"
element={
<ProtectedLoginRoute>
<SignUp />
</ProtectedLoginRoute>
}
/>
<Route
path="/login/admin"
element={
<ProtectedLoginRoute>
<LoginAdminPage />
</ProtectedLoginRoute>
}
/>
<Route path="/admin" element={<AdminPage />} />
<Route
path="/admin"
element={
<ProtectedAdminRoute>
<AdminPage />
</ProtectedAdminRoute>
}
/>
<Route path="/account">
<Route path="delete" element={<DeleteAccountPage />}></Route>
<Route
path="delete"
element={
<ProtectedRoute>
<DeleteAccountPage />
</ProtectedRoute>
}
></Route>
<Route
path="api-keys"
element={
<ProtectedRoute>
<ApiKeysPage />
</ProtectedRoute>
}
></Route>
</Route>
</Routes>
);

View file

@ -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;
};

View file

@ -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,6 +531,15 @@ 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;

View file

@ -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<void>;
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;
};

View file

@ -48,6 +48,8 @@ export type alertContextType = {
export type darkContextType = {
dark: {};
setDark: (newState: {}) => void;
stars: number;
setStars: (stars: number) => void;
};
export type locationContextType = {

View file

@ -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,
@ -61,6 +64,7 @@ import {
Undo,
Unplug,
Upload,
UserCog2,
UserMinus2,
UserPlus2,
Users2,
@ -291,5 +295,9 @@ export const nodeIconsLucide: iconsType = {
ChevronsLeft,
FaGithub,
FaApple,
EyeOff,
Eye,
UserCog2,
Key,
Unplug,
};