feat(create_user_button.tsx): allow admin to invite user to proxy via invite-links

makes it easier for proxy admin to debug what different roles can/can't do
This commit is contained in:
Krrish Dholakia 2024-06-05 15:55:39 -07:00
parent f790d41e7f
commit e78cf92610
13 changed files with 423 additions and 171 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -15,9 +15,9 @@ model_list:
# rpm: 10 # rpm: 10
# model_name: gpt-3.5-turbo-fake-model # model_name: gpt-3.5-turbo-fake-model
- litellm_params: - litellm_params:
api_base: https://openai-gpt-4-test-v-1.openai.azure.com/ api_base: https://openai-gpt-4-test-v-1.openai.azure.com
api_key: os.environ/AZURE_API_KEY api_key: os.environ/AZURE_API_KEY
api_version: '2023-05-15' api_version: 2024-02-15-preview
model: azure/chatgpt-v-2 model: azure/chatgpt-v-2
model_name: gpt-3.5-turbo model_name: gpt-3.5-turbo
- litellm_params: - litellm_params:
@ -26,10 +26,10 @@ model_list:
- litellm_params: - litellm_params:
api_base: https://openai-gpt-4-test-v-1.openai.azure.com/ api_base: https://openai-gpt-4-test-v-1.openai.azure.com/
api_key: os.environ/AZURE_API_KEY api_key: os.environ/AZURE_API_KEY
api_version: '2023-05-15' api_version: 2024-02-15-preview
model: azure/chatgpt-v-2 model: azure/chatgpt-v-2
drop_params: True drop_params: True
model_name: gpt-3.5-turbo-drop-params model_name: gpt-3.5-turbo
- model_name: tts - model_name: tts
litellm_params: litellm_params:
model: openai/tts-1 model: openai/tts-1
@ -37,6 +37,7 @@ model_list:
litellm_params: litellm_params:
api_base: https://openai-france-1234.openai.azure.com api_base: https://openai-france-1234.openai.azure.com
api_key: os.environ/AZURE_FRANCE_API_KEY api_key: os.environ/AZURE_FRANCE_API_KEY
api_version: 2024-02-15-preview
model: azure/gpt-turbo model: azure/gpt-turbo
- model_name: text-embedding - model_name: text-embedding
litellm_params: litellm_params:

View file

@ -190,6 +190,7 @@ class LiteLLMRoutes(enum.Enum):
"/model/info", "/model/info",
"/v2/model/info", "/v2/model/info",
"/v2/key/info", "/v2/key/info",
"/model_group/info",
] ]
# NOTE: ROUTES ONLY FOR MASTER KEY - only the Master Key should be able to Reset Spend # NOTE: ROUTES ONLY FOR MASTER KEY - only the Master Key should be able to Reset Spend
@ -1343,6 +1344,12 @@ class InvitationModel(LiteLLMBase):
updated_by: str updated_by: str
class InvitationClaim(LiteLLMBase):
invitation_link: str
user_id: str
password: str
class ConfigFieldInfo(LiteLLMBase): class ConfigFieldInfo(LiteLLMBase):
field_name: str field_name: str
field_value: Any field_value: Any

View file

@ -1311,7 +1311,9 @@ async def user_api_key_auth(
if user_id != valid_token.user_id: if user_id != valid_token.user_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="key not allowed to access this user's info", detail="key not allowed to access this user's info. user_id={}, key's user_id={}".format(
user_id, valid_token.user_id
),
) )
elif route == "/model/info": elif route == "/model/info":
# /model/info just shows models user has access to # /model/info just shows models user has access to
@ -1406,7 +1408,7 @@ async def user_api_key_auth(
"/global/predict/spend/logs", "/global/predict/spend/logs",
"/global/activity", "/global/activity",
"/health/services", "/health/services",
] ] + LiteLLMRoutes.info_routes.value
# check if the current route startswith any of the allowed routes # check if the current route startswith any of the allowed routes
if ( if (
route is not None route is not None
@ -9164,6 +9166,7 @@ async def new_user(data: NewUserRequest):
data_json["table_name"] = ( data_json["table_name"] = (
"user" # only create a user, don't create key if 'auto_create_key' set to False "user" # only create a user, don't create key if 'auto_create_key' set to False
) )
response = await generate_key_helper_fn(request_type="user", **data_json) response = await generate_key_helper_fn(request_type="user", **data_json)
# Admin UI Logic # Admin UI Logic
@ -12765,7 +12768,10 @@ async def login(request: Request):
_password = getattr(_user_row, "password", "unknown") _password = getattr(_user_row, "password", "unknown")
# check if password == _user_row.password # check if password == _user_row.password
if secrets.compare_digest(password, _password): hash_password = hash_token(token=password)
if secrets.compare_digest(password, _password) or secrets.compare_digest(
hash_password, _password
):
if os.getenv("DATABASE_URL") is not None: if os.getenv("DATABASE_URL") is not None:
response = await generate_key_helper_fn( response = await generate_key_helper_fn(
request_type="key", request_type="key",
@ -12928,6 +12934,92 @@ async def onboarding(invite_link: str):
} }
@app.post("/onboarding/claim_token", include_in_schema=False)
async def claim_onboarding_link(data: InvitationClaim):
"""
Special route. Allows UI link share user to update their password.
- Get the invite link
- Validate it's still 'valid'
- Check if user within initial session (prevents abuse)
- Get user from db
- Update user password
This route can only update user password.
"""
global prisma_client
### VALIDATE INVITE LINK ###
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
invite_obj = await prisma_client.db.litellm_invitationlink.find_unique(
where={"id": data.invitation_link}
)
if invite_obj is None:
raise HTTPException(
status_code=401, detail={"error": "Invitation link does not exist in db."}
)
#### CHECK IF EXPIRED
# Extract the date part from both datetime objects
utc_now_date = litellm.utils.get_utc_datetime().date()
expires_at_date = invite_obj.expires_at.date()
if expires_at_date < utc_now_date:
raise HTTPException(
status_code=401, detail={"error": "Invitation link has expired."}
)
#### CHECK IF CLAIMED
##### if claimed - check if within valid session (within 10 minutes of being claimed)
##### if unclaimed - reject
current_time = litellm.utils.get_utc_datetime()
if invite_obj.is_accepted == True:
time_difference = current_time - invite_obj.updated_at
# Check if the difference is within 10 minutes
if time_difference > timedelta(minutes=10):
raise HTTPException(
status_code=401,
detail={
"error": "The invitation link has already been claimed. Please ask your admin for a new invite link."
},
)
else:
raise HTTPException(
status_code=401,
detail={
"error": "The invitation link was never validated. Please file an issue, if this is not intended - https://github.com/BerriAI/litellm/issues."
},
)
#### CHECK IF VALID USER ID
if invite_obj.user_id != data.user_id:
raise HTTPException(
status_code=401,
detail={
"error": "Invalid invitation link. The user id submitted does not match the user id this link is attached to. Got={}, Expected={}".format(
data.user_id, invite_obj.user_id
)
},
)
### UPDATE USER OBJECT ###
hash_password = hash_token(token=data.password)
user_obj = await prisma_client.db.litellm_usertable.update(
where={"user_id": invite_obj.user_id}, data={"password": hash_password}
)
if user_obj is None:
raise HTTPException(
status_code=401, detail={"error": "User does not exist in db."}
)
return user_obj
@app.get("/get_image", include_in_schema=False) @app.get("/get_image", include_in_schema=False)
def get_image(): def get_image():
"""Get logo to show on admin UI""" """Get logo to show on admin UI"""

View file

@ -1,12 +1,22 @@
"use client"; "use client";
import React, { Suspense, useEffect, useState } from "react"; import React, { Suspense, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { Card, Title, Text, TextInput, Callout, Button, Grid, Col } from "@tremor/react"; import {
import { RiAlarmWarningLine, RiCheckboxCircleLine } from '@remixicon/react'; Card,
Title,
Text,
TextInput,
Callout,
Button,
Grid,
Col,
} from "@tremor/react";
import { RiAlarmWarningLine, RiCheckboxCircleLine } from "@remixicon/react";
import { import {
invitationClaimCall, invitationClaimCall,
userUpdateUserCall, userUpdateUserCall,
getOnboardingCredentials, getOnboardingCredentials,
claimOnboardingToken,
} from "@/components/networking"; } from "@/components/networking";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
import { Form, Button as Button2, message } from "antd"; import { Form, Button as Button2, message } from "antd";
@ -18,6 +28,7 @@ export default function Onboarding() {
const [accessToken, setAccessToken] = useState<string | null>(null); const [accessToken, setAccessToken] = useState<string | null>(null);
const [defaultUserEmail, setDefaultUserEmail] = useState<string>(""); const [defaultUserEmail, setDefaultUserEmail] = useState<string>("");
const [userEmail, setUserEmail] = useState<string>(""); const [userEmail, setUserEmail] = useState<string>("");
const [userID, setUserID] = useState<string | null>(null);
const [loginUrl, setLoginUrl] = useState<string>(""); const [loginUrl, setLoginUrl] = useState<string>("");
const [jwtToken, setJwtToken] = useState<string>(""); const [jwtToken, setJwtToken] = useState<string>("");
@ -30,7 +41,6 @@ export default function Onboarding() {
console.log("login_url:", login_url); console.log("login_url:", login_url);
setLoginUrl(login_url); setLoginUrl(login_url);
const token = data.token; const token = data.token;
const decoded = jwtDecode(token) as { [key: string]: any }; const decoded = jwtDecode(token) as { [key: string]: any };
setJwtToken(token); setJwtToken(token);
@ -42,31 +52,44 @@ export default function Onboarding() {
const user_email = decoded.user_email; const user_email = decoded.user_email;
setUserEmail(user_email); setUserEmail(user_email);
const user_id = decoded.user_id;
setUserID(user_id);
}); });
}, [inviteID]); }, [inviteID]);
const handleSubmit = (formValues: Record<string, any>) => { const handleSubmit = (formValues: Record<string, any>) => {
console.log("in handle submit. accessToken:", accessToken, "token:", jwtToken, "formValues:", formValues); console.log(
"in handle submit. accessToken:",
accessToken,
"token:",
jwtToken,
"formValues:",
formValues
);
if (!accessToken || !jwtToken) { if (!accessToken || !jwtToken) {
return; return;
} }
formValues.user_email = userEmail; formValues.user_email = userEmail;
userUpdateUserCall(accessToken, formValues, null).then((data) => { if (!userID || !inviteID) {
return;
}
claimOnboardingToken(
accessToken,
inviteID,
userID,
formValues.password
).then((data) => {
let litellm_dashboard_ui = "/ui/"; let litellm_dashboard_ui = "/ui/";
const user_id = data.data?.user_id || data.user_id; const user_id = data.data?.user_id || data.user_id;
litellm_dashboard_ui += "?userID=" + user_id + "&token=" + jwtToken; litellm_dashboard_ui += "?userID=" + user_id + "&token=" + jwtToken;
console.log("redirecting to:", litellm_dashboard_ui); console.log("redirecting to:", litellm_dashboard_ui);
window.location.href = litellm_dashboard_ui; window.location.href = litellm_dashboard_ui;
}); });
// redirect to login page // redirect to login page
}; };
return ( return (
<div className="mx-auto max-w-md mt-10"> <div className="mx-auto max-w-md mt-10">
@ -82,19 +105,15 @@ export default function Onboarding() {
color="sky" color="sky"
> >
<Grid numItems={2} className="flex justify-between items-center"> <Grid numItems={2} className="flex justify-between items-center">
<Col> <Col>SSO is under the Enterprise Tirer.</Col>
SSO is under the Enterprise Tirer.
</Col>
<Col> <Col>
<Button variant="primary" className="mb-2"> <Button variant="primary" className="mb-2">
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank"> <a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
Get Free Trial Get Free Trial
</a> </a>
</Button> </Button>
</Col> </Col>
</Grid> </Grid>
</Callout> </Callout>
@ -104,10 +123,7 @@ export default function Onboarding() {
onFinish={handleSubmit} onFinish={handleSubmit}
> >
<> <>
<Form.Item <Form.Item label="Email Address" name="user_email">
label="Email Address"
name="user_email"
>
<TextInput <TextInput
type="email" type="email"
disabled={true} disabled={true}
@ -120,7 +136,9 @@ export default function Onboarding() {
<Form.Item <Form.Item
label="Password" label="Password"
name="password" name="password"
rules={[{ required: true, message: "password required to sign up" }]} rules={[
{ required: true, message: "password required to sign up" },
]}
help="Create a password for your account" help="Create a password for your account"
> >
<TextInput placeholder="" type="password" className="max-w-md" /> <TextInput placeholder="" type="password" className="max-w-md" />

View file

@ -33,6 +33,8 @@ import {
Divider, Divider,
} from "@tremor/react"; } from "@tremor/react";
import { PencilAltIcon } from "@heroicons/react/outline"; import { PencilAltIcon } from "@heroicons/react/outline";
import OnboardingModal from "./onboarding_link";
import { InvitationLink } from "./onboarding_link";
interface AdminPanelProps { interface AdminPanelProps {
searchParams: any; searchParams: any;
accessToken: string | null; accessToken: string | null;
@ -40,17 +42,6 @@ interface AdminPanelProps {
showSSOBanner: boolean; showSSOBanner: boolean;
} }
interface InvitationLink {
id: string;
user_id: string;
is_accepted: boolean;
accepted_at: Date | null;
expires_at: Date;
created_at: Date;
created_by: string;
updated_at: Date;
updated_by: string;
}
import { import {
userUpdateUserCall, userUpdateUserCall,
Member, Member,
@ -84,11 +75,15 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
useState(false); useState(false);
const router = useRouter(); const router = useRouter();
const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<string, Record<string, string>>>(null); const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<
string,
Record<string, string>
>>(null);
const isLocal = process.env.NODE_ENV === "development"; const isLocal = process.env.NODE_ENV === "development";
const [baseUrl, setBaseUrl] = useState(isLocal ? "http://localhost:4000" : ""); const [baseUrl, setBaseUrl] = useState(
isLocal ? "http://localhost:4000" : ""
);
let nonSssoUrl; let nonSssoUrl;
try { try {
@ -346,7 +341,6 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
setIsInvitationLinkModalVisible(true); setIsInvitationLinkModalVisible(true);
}); });
const foundIndex = admins.findIndex((user) => { const foundIndex = admins.findIndex((user) => {
console.log( console.log(
`user.user_id=${user.user_id}; response.user_id=${response.user_id}` `user.user_id=${user.user_id}; response.user_id=${response.user_id}`
@ -462,7 +456,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
? member["user_id"] ? member["user_id"]
: null} : null}
</TableCell> </TableCell>
<TableCell> {possibleUIRoles?.[member?.user_role]?.ui_label || "-"}</TableCell> <TableCell>
{" "}
{possibleUIRoles?.[member?.user_role]?.ui_label ||
"-"}
</TableCell>
<TableCell> <TableCell>
<Icon <Icon
icon={PencilAltIcon} icon={PencilAltIcon}
@ -509,39 +507,12 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
> >
{addMemberForm(handleAdminCreate)} {addMemberForm(handleAdminCreate)}
</Modal> </Modal>
<Modal <OnboardingModal
title="Invitation Link" isInvitationLinkModalVisible={isInvitationLinkModalVisible}
visible={isInvitationLinkModalVisible} setIsInvitationLinkModalVisible={setIsInvitationLinkModalVisible}
width={800} baseUrl={baseUrl}
footer={null} invitationLinkData={invitationLinkData}
onOk={handleAdminOk} />
onCancel={handleAdminCancel}
>
{/* {JSON.stringify(invitationLinkData)} */}
<Paragraph>
Copy and send the generated link to onboard this user to the
proxy.
</Paragraph>
<div className="flex justify-between pt-5 pb-2">
<Text className="text-base">User ID</Text>
<Text>{invitationLinkData?.user_id}</Text>
</div>
<div className="flex justify-between pt-5 pb-2">
<Text>Invitation Link</Text>
<Text>
{baseUrl}/ui/onboarding?id={invitationLinkData?.id}
</Text>
</div>
<div className="flex justify-end mt-5">
<div></div>
<CopyToClipboard
text={`${baseUrl}/ui/onboarding?id=${invitationLinkData?.id}`}
onCopy={() => message.success("Copied!")}
>
<Button variant="primary">Copy invitation link</Button>
</CopyToClipboard>
</div>
</Modal>
<Button <Button
className="mb-5" className="mb-5"
onClick={() => setIsAddMemberModalVisible(true)} onClick={() => setIsAddMemberModalVisible(true)}

View file

@ -1,25 +1,51 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Button, Modal, Form, Input, message, Select, InputNumber } from "antd"; import { useRouter } from "next/navigation";
import { Button as Button2, Text, TextInput } from "@tremor/react"; import {
import { userCreateCall, modelAvailableCall } from "./networking"; Button,
Modal,
Form,
Input,
message,
Select,
InputNumber,
Select as Select2,
} from "antd";
import { Button as Button2, Text, TextInput, SelectItem } from "@tremor/react";
import OnboardingModal from "./onboarding_link";
import { InvitationLink } from "./onboarding_link";
import {
userCreateCall,
modelAvailableCall,
invitationCreateCall,
} from "./networking";
const { Option } = Select; const { Option } = Select;
interface CreateuserProps { interface CreateuserProps {
userID: string; userID: string;
accessToken: string; accessToken: string;
teams: any[] | null; teams: any[] | null;
possibleUIRoles: null | Record<string, Record<string, string>>;
} }
const Createuser: React.FC<CreateuserProps> = ({ const Createuser: React.FC<CreateuserProps> = ({
userID, userID,
accessToken, accessToken,
teams, teams,
possibleUIRoles,
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [apiuser, setApiuser] = useState<string | null>(null); const [apiuser, setApiuser] = useState<string | null>(null);
const [userModels, setUserModels] = useState<string[]>([]); const [userModels, setUserModels] = useState<string[]>([]);
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] =
useState(false);
const [invitationLinkData, setInvitationLinkData] =
useState<InvitationLink | null>(null);
const router = useRouter();
const isLocal = process.env.NODE_ENV === "development";
const [baseUrl, setBaseUrl] = useState(
isLocal ? "http://localhost:4000" : ""
);
// get all models // get all models
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@ -48,6 +74,14 @@ const Createuser: React.FC<CreateuserProps> = ({
fetchData(); // Call the function to fetch model data when the component mounts fetchData(); // Call the function to fetch model data when the component mounts
}, []); // Empty dependency array to run only once }, []); // Empty dependency array to run only once
useEffect(() => {
if (router) {
const { protocol, host } = window.location;
const baseUrl = `${protocol}/${host}`;
setBaseUrl(baseUrl);
}
}, [router]);
const handleOk = () => { const handleOk = () => {
setIsModalVisible(false); setIsModalVisible(false);
form.resetFields(); form.resetFields();
@ -67,6 +101,11 @@ const Createuser: React.FC<CreateuserProps> = ({
const response = await userCreateCall(accessToken, null, formValues); const response = await userCreateCall(accessToken, null, formValues);
console.log("user create Response:", response); console.log("user create Response:", response);
setApiuser(response["key"]); setApiuser(response["key"]);
const user_id = response.data?.user_id || response.user_id;
invitationCreateCall(accessToken, user_id).then((data) => {
setInvitationLinkData(data);
setIsInvitationLinkModalVisible(true);
});
message.success("API user Created"); message.success("API user Created");
form.resetFields(); form.resetFields();
localStorage.removeItem("userData" + userID); localStorage.removeItem("userData" + userID);
@ -88,12 +127,7 @@ const Createuser: React.FC<CreateuserProps> = ({
onOk={handleOk} onOk={handleOk}
onCancel={handleCancel} onCancel={handleCancel}
> >
<Text className="mb-1"> <Text className="mb-1">Create a User who can own keys</Text>
Invite a user to login to the Admin UI and create Keys
</Text>
<Text className="mb-6">
<b>Note: SSO Setup Required for this</b>
</Text>
<Form <Form
form={form} form={form}
onFinish={handleCreate} onFinish={handleCreate}
@ -104,6 +138,26 @@ const Createuser: React.FC<CreateuserProps> = ({
<Form.Item label="User Email" name="user_email"> <Form.Item label="User Email" name="user_email">
<TextInput placeholder="" /> <TextInput placeholder="" />
</Form.Item> </Form.Item>
<Form.Item label="User Role" name="user_role">
<Select2>
{possibleUIRoles &&
Object.entries(possibleUIRoles).map(
([role, { ui_label, description }]) => (
<SelectItem key={role} value={role} title={ui_label}>
<div className="flex">
{ui_label}{" "}
<p
className="ml-2"
style={{ color: "gray", fontSize: "12px" }}
>
{description}
</p>
</div>
</SelectItem>
)
)}
</Select2>
</Form.Item>
<Form.Item label="Team ID" name="team_id"> <Form.Item label="Team ID" name="team_id">
<Select placeholder="Select Team ID" style={{ width: "100%" }}> <Select placeholder="Select Team ID" style={{ width: "100%" }}>
{teams ? ( {teams ? (
@ -119,6 +173,7 @@ const Createuser: React.FC<CreateuserProps> = ({
)} )}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item label="Metadata" name="metadata"> <Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" /> <Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
</Form.Item> </Form.Item>
@ -128,25 +183,12 @@ const Createuser: React.FC<CreateuserProps> = ({
</Form> </Form>
</Modal> </Modal>
{apiuser && ( {apiuser && (
<Modal <OnboardingModal
title="User Created Successfully" isInvitationLinkModalVisible={isInvitationLinkModalVisible}
visible={isModalVisible} setIsInvitationLinkModalVisible={setIsInvitationLinkModalVisible}
onOk={handleOk} baseUrl={baseUrl}
onCancel={handleCancel} invitationLinkData={invitationLinkData}
footer={null} />
>
<p>
User has been created to access your proxy. Please Ask them to Log
In.
</p>
<br></br>
<p>
<b>
Note: This Feature is only supported through SSO on the Admin UI
</b>
</p>
</Modal>
)} )}
</div> </div>
); );

View file

@ -522,6 +522,12 @@ export const userInfoCall = async (
if (userRole == "App User" && userID) { if (userRole == "App User" && userID) {
url = `${url}?user_id=${userID}`; url = `${url}?user_id=${userID}`;
} }
if (
(userRole == "Internal User" || userRole == "Internal Viewer") &&
userID
) {
url = `${url}?user_id=${userID}`;
}
console.log("in userInfoCall viewAll=", viewAll); console.log("in userInfoCall viewAll=", viewAll);
if (viewAll && page_size && page != null && page != undefined) { if (viewAll && page_size && page != null && page != undefined) {
url = `${url}?view_all=true&page=${page}&page_size=${page_size}`; url = `${url}?view_all=true&page=${page}&page_size=${page_size}`;
@ -617,14 +623,15 @@ export const getTotalSpendCall = async (accessToken: String) => {
} }
}; };
export const getOnboardingCredentials = async (inviteUUID: String) => { export const getOnboardingCredentials = async (inviteUUID: String) => {
/** /**
* Get all models on proxy * Get all models on proxy
*/ */
try { try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/onboarding/get_token` : `/onboarding/get_token`; let url = proxyBaseUrl
url += `?invite_link=${inviteUUID}` ? `${proxyBaseUrl}/onboarding/get_token`
: `/onboarding/get_token`;
url += `?invite_link=${inviteUUID}`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
@ -648,6 +655,43 @@ export const getOnboardingCredentials = async (inviteUUID: String) => {
} }
}; };
export const claimOnboardingToken = async (
accessToken: string,
inviteUUID: string,
userID: string,
password: String
) => {
const url = proxyBaseUrl
? `${proxyBaseUrl}/onboarding/claim_token`
: `/onboarding/claim_token`;
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
invitation_link: inviteUUID,
user_id: userID,
password: password,
}),
});
if (!response.ok) {
const errorData = await response.text();
message.error("Failed to delete team: " + errorData, 10);
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log(data);
return data;
// Handle success - you might want to update some state or UI based on the created key
} catch (error) {
console.error("Failed to delete key:", error);
throw error;
}
};
export const modelInfoCall = async ( export const modelInfoCall = async (
accessToken: String, accessToken: String,
userID: String, userID: String,
@ -1024,15 +1068,12 @@ export const tagsSpendLogsCall = async (
} }
}; };
export const allTagNamesCall = async ( export const allTagNamesCall = async (accessToken: String) => {
accessToken: String,
) => {
try { try {
let url = proxyBaseUrl let url = proxyBaseUrl
? `${proxyBaseUrl}/global/spend/all_tag_names` ? `${proxyBaseUrl}/global/spend/all_tag_names`
: `/global/spend/all_tag_names`; : `/global/spend/all_tag_names`;
console.log("in global/spend/all_tag_names call", url); console.log("in global/spend/all_tag_names call", url);
const response = await fetch(`${url}`, { const response = await fetch(`${url}`, {
method: "GET", method: "GET",
@ -1055,16 +1096,12 @@ export const allTagNamesCall = async (
} }
}; };
export const allEndUsersCall = async (accessToken: String) => {
export const allEndUsersCall = async (
accessToken: String,
) => {
try { try {
let url = proxyBaseUrl let url = proxyBaseUrl
? `${proxyBaseUrl}/global/all_end_users` ? `${proxyBaseUrl}/global/all_end_users`
: `/global/all_end_users`; : `/global/all_end_users`;
console.log("in global/all_end_users call", url); console.log("in global/all_end_users call", url);
const response = await fetch(`${url}`, { const response = await fetch(`${url}`, {
method: "GET", method: "GET",
@ -1087,7 +1124,6 @@ export const allEndUsersCall = async (
} }
}; };
export const userSpendLogsCall = async ( export const userSpendLogsCall = async (
accessToken: String, accessToken: String,
token: String, token: String,
@ -1377,13 +1413,11 @@ export const adminGlobalActivityPerModel = async (
} }
}; };
export const adminGlobalActivityExceptions = async ( export const adminGlobalActivityExceptions = async (
accessToken: String, accessToken: String,
startTime: String | undefined, startTime: String | undefined,
endTime: String | undefined, endTime: String | undefined,
modelGroup: String, modelGroup: String
) => { ) => {
try { try {
let url = proxyBaseUrl let url = proxyBaseUrl
@ -1429,7 +1463,7 @@ export const adminGlobalActivityExceptionsPerDeployment = async (
accessToken: String, accessToken: String,
startTime: String | undefined, startTime: String | undefined,
endTime: String | undefined, endTime: String | undefined,
modelGroup: String, modelGroup: String
) => { ) => {
try { try {
let url = proxyBaseUrl let url = proxyBaseUrl
@ -1666,9 +1700,7 @@ export const userGetAllUsersCall = async (
} }
}; };
export const getPossibleUserRoles = async ( export const getPossibleUserRoles = async (accessToken: String) => {
accessToken: String,
) => {
try { try {
const url = proxyBaseUrl const url = proxyBaseUrl
? `${proxyBaseUrl}/user/available_roles` ? `${proxyBaseUrl}/user/available_roles`

View file

@ -0,0 +1,86 @@
import React, { useState, useEffect } from "react";
import {
Button as Button2,
Modal,
Form,
Input,
Select as Select2,
InputNumber,
message,
Typography,
} from "antd";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { Text, Button } from "@tremor/react";
export interface InvitationLink {
id: string;
user_id: string;
is_accepted: boolean;
accepted_at: Date | null;
expires_at: Date;
created_at: Date;
created_by: string;
updated_at: Date;
updated_by: string;
}
interface OnboardingProps {
isInvitationLinkModalVisible: boolean;
setIsInvitationLinkModalVisible: React.Dispatch<
React.SetStateAction<boolean>
>;
baseUrl: string;
invitationLinkData: InvitationLink | null;
}
const OnboardingModal: React.FC<OnboardingProps> = ({
isInvitationLinkModalVisible,
setIsInvitationLinkModalVisible,
baseUrl,
invitationLinkData,
}) => {
const { Title, Paragraph } = Typography;
const handleInvitationOk = () => {
setIsInvitationLinkModalVisible(false);
};
const handleInvitationCancel = () => {
setIsInvitationLinkModalVisible(false);
};
return (
<Modal
title="Invitation Link"
visible={isInvitationLinkModalVisible}
width={800}
footer={null}
onOk={handleInvitationOk}
onCancel={handleInvitationCancel}
>
{/* {JSON.stringify(invitationLinkData)} */}
<Paragraph>
Copy and send the generated link to onboard this user to the proxy.
</Paragraph>
<div className="flex justify-between pt-5 pb-2">
<Text className="text-base">User ID</Text>
<Text>{invitationLinkData?.user_id}</Text>
</div>
<div className="flex justify-between pt-5 pb-2">
<Text>Invitation Link</Text>
<Text>
{baseUrl}/ui/onboarding?id={invitationLinkData?.id}
</Text>
</div>
<div className="flex justify-end mt-5">
<div></div>
<CopyToClipboard
text={`${baseUrl}/ui/onboarding?id=${invitationLinkData?.id}`}
onCopy={() => message.success("Copied!")}
>
<Button variant="primary">Copy invitation link</Button>
</CopyToClipboard>
</div>
</Modal>
);
};
export default OnboardingModal;

View file

@ -101,7 +101,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
return "App User"; return "App User";
case "internal_user": case "internal_user":
return "Internal User"; return "Internal User";
case "internal_viewer": case "internal_user_viewer":
return "Internal Viewer"; return "Internal Viewer";
default: default:
return "Unknown Role"; return "Unknown Role";

View file

@ -25,11 +25,13 @@ import {
TextInput, TextInput,
} from "@tremor/react"; } from "@tremor/react";
import { import { message } from "antd";
message,
} from "antd";
import { userInfoCall, userUpdateUserCall, getPossibleUserRoles } from "./networking"; import {
userInfoCall,
userUpdateUserCall,
getPossibleUserRoles,
} from "./networking";
import { Badge, BadgeDelta, Button } from "@tremor/react"; import { Badge, BadgeDelta, Button } from "@tremor/react";
import RequestAccess from "./request_model_access"; import RequestAccess from "./request_model_access";
import CreateUser from "./create_user_button"; import CreateUser from "./create_user_button";
@ -67,7 +69,9 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
const [selectedItem, setSelectedItem] = useState<null | any>(null); const [selectedItem, setSelectedItem] = useState<null | any>(null);
const [editModalVisible, setEditModalVisible] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
const [possibleUIRoles, setPossibleUIRoles] = useState<Record<string, Record<string, string>>>({}); const [possibleUIRoles, setPossibleUIRoles] = useState<
Record<string, Record<string, string>>
>({});
const defaultPageSize = 25; const defaultPageSize = 25;
const handleEditCancel = async () => { const handleEditCancel = async () => {
@ -119,13 +123,11 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
const availableUserRoles = await getPossibleUserRoles(accessToken); const availableUserRoles = await getPossibleUserRoles(accessToken);
setPossibleUIRoles(availableUserRoles); setPossibleUIRoles(availableUserRoles);
} catch (error) { } catch (error) {
console.error("There was an error fetching the model data", error); console.error("There was an error fetching the model data", error);
} }
}; };
if (accessToken && token && userRole && userID) { if (accessToken && token && userRole && userID) {
fetchData(); fetchData();
} }
@ -174,10 +176,14 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
return ( return (
<div style={{ width: "100%" }}> <div style={{ width: "100%" }}>
<Grid className="gap-2 p-2 h-[90vh] w-full mt-8"> <Grid className="gap-2 p-2 h-[90vh] w-full mt-8">
<CreateUser userID={userID} accessToken={accessToken} teams={teams} /> <CreateUser
userID={userID}
accessToken={accessToken}
teams={teams}
possibleUIRoles={possibleUIRoles}
/>
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[90vh] mb-4"> <Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[90vh] mb-4">
<div className="mb-4 mt-1"> <div className="mb-4 mt-1"></div>
</div>
<TabGroup> <TabGroup>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
@ -218,7 +224,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
user.key_aliases.filter( user.key_aliases.filter(
(key: any) => key !== null (key: any) => key !== null
).length ).length
} }
&nbsp;Keys &nbsp;Keys
</Badge> </Badge>
@ -236,20 +241,21 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
</Grid> </Grid>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Icon
<Icon icon={PencilAltIcon} onClick= {() => { icon={PencilAltIcon}
setSelectedUser(user) onClick={() => {
setEditModalVisible(true) setSelectedUser(user);
}}>View Keys</Icon> setEditModalVisible(true);
{/* }}
>
View Keys
</Icon>
{/*
<Icon icon={TrashIcon} onClick= {() => { <Icon icon={TrashIcon} onClick= {() => {
setOpenDialogId(user.user_id) setOpenDialogId(user.user_id)
setSelectedItem(user) setSelectedItem(user)
}}>View Keys</Icon> */} }}>View Keys</Icon> */}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>