diff --git a/src/backend/langflow/auth/auth.py b/src/backend/langflow/auth/auth.py index 28d242342..9d4f12862 100644 --- a/src/backend/langflow/auth/auth.py +++ b/src/backend/langflow/auth/auth.py @@ -7,9 +7,8 @@ from fastapi.security import OAuth2PasswordBearer from fastapi import Depends, HTTPException, status from datetime import datetime, timedelta, timezone -from langflow.services.utils import get_settings_manager +from langflow.services.utils import get_settings_manager, get_session -from langflow.services.utils import get_session from langflow.database.models.user import ( User, get_user_by_id, @@ -125,6 +124,23 @@ def create_user_longterm_token(db: Session = Depends(get_session)) -> dict: } +def create_user_api_key(user_id: UUID) -> dict: + access_token = create_token( + data={"sub": str(user_id), "role": "api_key"}, + expires_delta=timedelta(days=365 * 2), + ) + + return {"api_key": access_token} + + +def get_user_id_from_token(token: str) -> UUID: + try: + user_id = jwt.get_unverified_claims(token)["sub"] + return UUID(user_id) + except (KeyError, JWTError, ValueError): + return UUID(int=0) + + def create_user_tokens( user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False ) -> dict: diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index b63caff24..7045ec99d 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -6,7 +6,7 @@ from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from langflow.api import router -from langflow.routers import login, users, health +from langflow.routers import api_key, login, users, health from langflow.interface.utils import setup_llm_caching from langflow.services.database.utils import initialize_database @@ -32,6 +32,7 @@ def create_app(): ) app.include_router(login.router) + app.include_router(api_key.router) app.include_router(users.router) app.include_router(health.router) diff --git a/src/backend/langflow/routers/api_key.py b/src/backend/langflow/routers/api_key.py new file mode 100644 index 000000000..7cc712962 --- /dev/null +++ b/src/backend/langflow/routers/api_key.py @@ -0,0 +1,49 @@ + + +from fastapi import APIRouter + + + +router = APIRouter(tags=["APIKey"]) + + +@router.get("/api_key/{user_id}") +def get_api_key(user_id: str): + return { + "total_count": 3, + "user_id": user_id, + "api_keys": [ + { + "id": "4425707e-cce4-4d1b-a54e-bd2632064657", + "name": "my api_key name - 01", + "created_at": "2023-08-15T19:28:40.019613", + "last_used_at": "2023-08-16T18:38:20.875210", + }, + { + "id": "6fb7282b-9f2e-4efe-9bda-0c3d8f899473", + "name": "my api_key name - 02", + "created_at": "2023-08-15T19:41:30.077942", + "last_used_at": "2023-08-15T19:45:32.067899", + }, + { + "id": "c55f3b32-4920-42b6-a5cd-698b4251806e", + "name": "my api_key name - 03", + "created_at": "2023-08-15T20:29:40.577808", + "last_used_at": "2023-08-15T20:29:40.577816", + }, + ], + } + + +@router.post("/api_key/{user_id}") +def create_api_key(user_id: str): + return { + "user_id": user_id, + "name": "my api-key 01", + "api_key": "lf-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YTBmODM1ZS0yMTQxLTQ2YWItYmQ4NS0yMWEzMjQ1MTE2ZDAiLCJleHAiOjE2OTIyMTUwMTN9.c_s0ZPRtjSI9yUrhi8ACIwyXf0feRLYfaeIZEbRVKQg", + } + + +@router.delete("/api_key/{api_key_id}") +def delete_api_key(api_key_id: str): + return {"detail": "API Key deleted"} diff --git a/src/backend/langflow/services/settings/base.py b/src/backend/langflow/services/settings/base.py index d99f0f8b5..ec976bade 100644 --- a/src/backend/langflow/services/settings/base.py +++ b/src/backend/langflow/services/settings/base.py @@ -42,11 +42,17 @@ class Settings(BaseSettings): ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 REFRESH_TOKEN_EXPIRE_MINUTES: int = 70 + # API Key to execute /process endpoint + API_KEY_SECRET_KEY: Optional[ + str + ] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a" + API_KEY_ALGORITHM: str = "HS256" + # If AUTO_LOGIN = True # > The application does not request login and logs in automatically as a super user. - AUTO_LOGIN: bool = False - FIRST_SUPERUSER: str = "superuser" - FIRST_SUPERUSER_PASSWORD: str = "12345" + AUTO_LOGIN: bool = True + FIRST_SUPERUSER: str = "langflow" + FIRST_SUPERUSER_PASSWORD: str = "langflow" @validator("DATABASE_URL", pre=True) def set_database_url(cls, value): diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index beab8c683..1949b38dc 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -136,26 +136,22 @@ export default function App() { useEffect(() => { setTimeout(() => { - autoLogin() - .then((user) => { - if (user && user["access_token"]) { - user["refresh_token"] = "auto"; - login(user["access_token"], user["refresh_token"]); - setAutoLogin(true); - } - }) - .catch((error) => { - setAutoLogin(false); - if (getAuthentication && !isLoginPage) { - getLoggedUser() - .then((user) => { - setUserData(user); - }) - .catch((error) => {}); - } else { - navigate("/login"); - } - }); + autoLogin().then((user) => { + if(user && user['access_token']){ + user['refresh_token'] = "auto"; + login(user['access_token'], user['refresh_token']); + setAutoLogin(true); + } + }).catch((error) => { + setAutoLogin(false); + if (getAuthentication && !isLoginPage) { + getLoggedUser() + .then((user) => { + setUserData(user); + }) + .catch((error) => {}); + } + }); }, 500); }, []); diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 105e75b23..67006ef05 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -528,6 +528,10 @@ export const CONTROL_NEW_USER = { is_superuser: false, }; +export const CONTROL_NEW_API_KEY = { + apikeyname: "", +}; + export const tabsCode = []; export function tabsArray(codes: string[], method: number) { diff --git a/src/frontend/src/modals/SecretKeyModal/index.tsx b/src/frontend/src/modals/SecretKeyModal/index.tsx new file mode 100644 index 000000000..a5a3143f6 --- /dev/null +++ b/src/frontend/src/modals/SecretKeyModal/index.tsx @@ -0,0 +1,191 @@ +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 { + 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, + index, + onConfirm, +}: ApiKeyType) { + const Icon: any = nodeIconsLucide[icon]; + const [open, setOpen] = useState(false); + const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? ""); + const [apiKeyValue, setApiKeyValue] = useState("Value"); + 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(); + } + }, [open]); + + function resetForm() { + setApiKeyName(""); + setApiKeyValue("Value"); + } + + 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); + } + }; + + 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} + /> +
+ +
+ +
+
+ + )} + + { + setRenderKey(true); + event.preventDefault(); + }} + > + {renderKey === false && ( +
+ +
+ + Name (optional){" "} + +
+ + { + handleInput({ target: { name: "apikeyname", value } }); + setApiKeyName(value); + }} + value={apiKeyName} + className="primary-input" + placeholder="My key name" + /> + +
+
+ )} + {renderKey === false && ( +
+ + + + + +
+ )} + + {renderKey === true && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/src/frontend/src/modals/UserManagementModal/index.tsx b/src/frontend/src/modals/UserManagementModal/index.tsx index d370ef841..993ffb944 100644 --- a/src/frontend/src/modals/UserManagementModal/index.tsx +++ b/src/frontend/src/modals/UserManagementModal/index.tsx @@ -273,17 +273,20 @@ export default function UserManagementModal({
- - - - + + + + +
diff --git a/src/frontend/src/pages/ApiKeysPage/index.tsx b/src/frontend/src/pages/ApiKeysPage/index.tsx new file mode 100644 index 000000000..a08e155f4 --- /dev/null +++ b/src/frontend/src/pages/ApiKeysPage/index.tsx @@ -0,0 +1,349 @@ +import { cloneDeep } from "lodash"; +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 { + addUser, + deleteUser, + getUsersPage, + updateUser, +} from "../../controllers/API"; +import ConfirmationModal from "../../modals/ConfirmationModal"; +import SecretKeyModal from "../../modals/SecretKeyModal"; +import { UserInputType } from "../../types/components"; + +export default function ApiKeysPage() { + const [inputValue, setInputValue] = useState(""); + + const [size, setPageSize] = useState(10); + 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([]); + + useEffect(() => { + setTimeout(() => { + getUsers(); + }, 500); + }, []); + + const [filterUserList, setFilterUserList] = useState(userList.current); + + 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 handleChangePagination(pageIndex: number, pageSize: number) { + 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() { + setPageIndex(0); + setPageSize(10); + getUsers(); + } + + function handleFilterUsers(input: string) { + setInputValue(input); + + if (input === "") { + setFilterUserList(userList.current); + } else { + const filteredList = userList.current.filter((user) => + user.username.toLowerCase().includes(input.toLowerCase()) + ); + setFilterUserList(filteredList); + } + } + + 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(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 handleDisableUser(check, userId, user) { + const userEdit = cloneDeep(user); + userEdit.is_active = !check; + + updateUser(userId, userEdit) + .then((res) => { + console.log(res); + + resetFilter(); + setSuccessData({ + title: "Success! User edited!", + }); + }) + .catch((error) => { + setErrorData({ + title: "Error on edit user", + list: [error["response"]["data"]["detail"]], + }); + }); + } + + 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"]], + }); + }); + } + + function lastUsedMessage() { + return ( +
+ + The last time this key was used.

Accurate to within the hour + from the most recent usage. +
+
+ ); + } + + return ( + <> + {userData && ( +
+
+
+
+
+
+

+ API keys +

+

+ Your secret API keys are listed below. Please note that we + do not display your secret API keys again after you + generate them.

+ Do not share your API key with others, or expose it in the + browser or other client-side code. +

+
+
+
+ + {userList.current.length === 0 && !loadingUsers && ( + <> +
+

There's no users registered :)

+
+ + )} + <> + {loadingUsers && ( +
+ Loading... +
+ )} +
+ + + + Name + Key + Created + + Last Used + +
+ +
+
+
+ +
+
+ {!loadingUsers && ( + + {filterUserList.map((user, index) => ( + + + + + {user.id} + + + + + + + {user.username} + + + + + { + new Date(user.create_at) + .toISOString() + .split("T")[0] + } + + + { + new Date(user.updated_at) + .toISOString() + .split("T")[0] + } + + +
+ { + handleDeleteUser(user); + }} + > + + + + +
+
+
+ ))} +
+ )} +
+
+ +
+
+ { + handleNewUser(user); + }} + > + + +
+
+ +
+
+
+
+ )} + + ); +} diff --git a/src/frontend/src/routes.tsx b/src/frontend/src/routes.tsx index 81421d057..88d485155 100644 --- a/src/frontend/src/routes.tsx +++ b/src/frontend/src/routes.tsx @@ -11,6 +11,7 @@ import HomePage from "./pages/MainPage"; import DeleteAccountPage from "./pages/deleteAccountPage"; import LoginPage from "./pages/loginPage"; import SignUp from "./pages/signUpPage"; +import ApiKeysPage from "./pages/ApiKeysPage"; const Router = () => { return ( @@ -93,6 +94,14 @@ const Router = () => { } > + + + + } + > ); diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index ddac47d9f..053eb50df 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -233,3 +233,18 @@ export type UserInputType = { is_active?: boolean; is_superuser?: boolean; }; + +export type ApiKeyType = { + title: string; + cancelText: string; + confirmationText: string; + children: ReactElement; + icon: string; + data?: any; + index?: number; + onConfirm: (index, data) => void; +}; + +export type ApiKeyInputType = { + apikeyname: string; +}; \ No newline at end of file diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index dac5b8e25..5b7e682a7 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -296,5 +296,5 @@ export const nodeIconsLucide = { EyeOff, Eye, UserCog2, - Key, + Key };