Resolve merge conflict

This commit is contained in:
igorrCarvalho 2023-08-16 18:21:53 -03:00
commit 6671a6c369
12 changed files with 670 additions and 31 deletions

View file

@ -7,9 +7,8 @@ from fastapi.security import OAuth2PasswordBearer
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from datetime import datetime, timedelta, timezone 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 ( from langflow.database.models.user import (
User, User,
get_user_by_id, 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( def create_user_tokens(
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
) -> dict: ) -> dict:

View file

@ -6,7 +6,7 @@ from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from langflow.api import router 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.interface.utils import setup_llm_caching
from langflow.services.database.utils import initialize_database from langflow.services.database.utils import initialize_database
@ -32,6 +32,7 @@ def create_app():
) )
app.include_router(login.router) app.include_router(login.router)
app.include_router(api_key.router)
app.include_router(users.router) app.include_router(users.router)
app.include_router(health.router) app.include_router(health.router)

View file

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

View file

@ -42,11 +42,17 @@ class Settings(BaseSettings):
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_MINUTES: int = 70 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 # If AUTO_LOGIN = True
# > The application does not request login and logs in automatically as a super user. # > The application does not request login and logs in automatically as a super user.
AUTO_LOGIN: bool = False AUTO_LOGIN: bool = True
FIRST_SUPERUSER: str = "superuser" FIRST_SUPERUSER: str = "langflow"
FIRST_SUPERUSER_PASSWORD: str = "12345" FIRST_SUPERUSER_PASSWORD: str = "langflow"
@validator("DATABASE_URL", pre=True) @validator("DATABASE_URL", pre=True)
def set_database_url(cls, value): def set_database_url(cls, value):

View file

@ -136,26 +136,22 @@ export default function App() {
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
autoLogin() autoLogin().then((user) => {
.then((user) => { if(user && user['access_token']){
if (user && user["access_token"]) { user['refresh_token'] = "auto";
user["refresh_token"] = "auto"; login(user['access_token'], user['refresh_token']);
login(user["access_token"], user["refresh_token"]); setAutoLogin(true);
setAutoLogin(true); }
} }).catch((error) => {
}) setAutoLogin(false);
.catch((error) => { if (getAuthentication && !isLoginPage) {
setAutoLogin(false); getLoggedUser()
if (getAuthentication && !isLoginPage) { .then((user) => {
getLoggedUser() setUserData(user);
.then((user) => { })
setUserData(user); .catch((error) => {});
}) }
.catch((error) => {}); });
} else {
navigate("/login");
}
});
}, 500); }, 500);
}, []); }, []);

View file

@ -528,6 +528,10 @@ export const CONTROL_NEW_USER = {
is_superuser: false, is_superuser: false,
}; };
export const CONTROL_NEW_API_KEY = {
apikeyname: "",
};
export const tabsCode = []; export const tabsCode = [];
export function tabsArray(codes: string[], method: number) { export function tabsArray(codes: string[], method: number) {

View file

@ -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<ApiKeyInputType>(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 (
<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);
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

@ -273,17 +273,20 @@ export default function UserManagementModal({
</div> </div>
<div className="float-right"> <div className="float-right">
<Form.Submit asChild> <Button
<Button className="mr-3 mt-8">{confirmationText}</Button>
</Form.Submit>
<Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
}} }}
className="mr-3"
> >
{cancelText} {cancelText}
</Button> </Button>
<Form.Submit asChild>
<Button className="mt-8">{confirmationText}</Button>
</Form.Submit>
</div> </div>
</Form.Root> </Form.Root>
</BaseModal.Content> </BaseModal.Content>

View file

@ -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 (
<div className="text-xs">
<span>
The last time this key was used.<br></br> Accurate to within the hour
from the most recent usage.
</span>
</div>
);
}
return (
<>
{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">
Your secret API keys are listed below. Please note that we
do not display your secret API keys again after you
generate them.<br></br>
Do not share your API key with others, or expose it in the
browser or other client-side code.
</p>
</div>
<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>
</>
)}
<>
{loadingUsers && (
<div>
<strong>Loading...</strong>
</div>
)}
<div
className={
"max-h-[15rem] min-h-[15rem] 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
className={
loadingUsers
? "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 w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
{!loadingUsers && (
<TableBody>
{filterUserList.map((user, index) => (
<TableRow key={index}>
<TableCell className="truncate py-2">
<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="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">
<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>
<div className="flex items-center justify-between">
<div>
<SecretKeyModal
title="Create new secret key"
cancelText="Cancel"
confirmationText="Create secret key"
icon={"Key"}
onConfirm={(index, user) => {
handleNewUser(user);
}}
>
<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

@ -11,6 +11,7 @@ import HomePage from "./pages/MainPage";
import DeleteAccountPage from "./pages/deleteAccountPage"; import DeleteAccountPage from "./pages/deleteAccountPage";
import LoginPage from "./pages/loginPage"; import LoginPage from "./pages/loginPage";
import SignUp from "./pages/signUpPage"; import SignUp from "./pages/signUpPage";
import ApiKeysPage from "./pages/ApiKeysPage";
const Router = () => { const Router = () => {
return ( return (
@ -93,6 +94,14 @@ const Router = () => {
</ProtectedRoute> </ProtectedRoute>
} }
></Route> ></Route>
<Route
path="api-keys"
element={
<ProtectedRoute>
<ApiKeysPage />
</ProtectedRoute>
}
></Route>
</Route> </Route>
</Routes> </Routes>
); );

View file

@ -233,3 +233,18 @@ export type UserInputType = {
is_active?: boolean; is_active?: boolean;
is_superuser?: 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;
};

View file

@ -296,5 +296,5 @@ export const nodeIconsLucide = {
EyeOff, EyeOff,
Eye, Eye,
UserCog2, UserCog2,
Key, Key
}; };