forked from phoenix/litellm-mirror
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:
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
|
@ -15,9 +15,9 @@ model_list:
|
|||
# rpm: 10
|
||||
# model_name: gpt-3.5-turbo-fake-model
|
||||
- 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_version: '2023-05-15'
|
||||
api_version: 2024-02-15-preview
|
||||
model: azure/chatgpt-v-2
|
||||
model_name: gpt-3.5-turbo
|
||||
- litellm_params:
|
||||
|
@ -26,10 +26,10 @@ model_list:
|
|||
- litellm_params:
|
||||
api_base: https://openai-gpt-4-test-v-1.openai.azure.com/
|
||||
api_key: os.environ/AZURE_API_KEY
|
||||
api_version: '2023-05-15'
|
||||
api_version: 2024-02-15-preview
|
||||
model: azure/chatgpt-v-2
|
||||
drop_params: True
|
||||
model_name: gpt-3.5-turbo-drop-params
|
||||
model_name: gpt-3.5-turbo
|
||||
- model_name: tts
|
||||
litellm_params:
|
||||
model: openai/tts-1
|
||||
|
@ -37,6 +37,7 @@ model_list:
|
|||
litellm_params:
|
||||
api_base: https://openai-france-1234.openai.azure.com
|
||||
api_key: os.environ/AZURE_FRANCE_API_KEY
|
||||
api_version: 2024-02-15-preview
|
||||
model: azure/gpt-turbo
|
||||
- model_name: text-embedding
|
||||
litellm_params:
|
||||
|
|
|
@ -190,6 +190,7 @@ class LiteLLMRoutes(enum.Enum):
|
|||
"/model/info",
|
||||
"/v2/model/info",
|
||||
"/v2/key/info",
|
||||
"/model_group/info",
|
||||
]
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
class InvitationClaim(LiteLLMBase):
|
||||
invitation_link: str
|
||||
user_id: str
|
||||
password: str
|
||||
|
||||
|
||||
class ConfigFieldInfo(LiteLLMBase):
|
||||
field_name: str
|
||||
field_value: Any
|
||||
|
|
|
@ -1311,7 +1311,9 @@ async def user_api_key_auth(
|
|||
if user_id != valid_token.user_id:
|
||||
raise HTTPException(
|
||||
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":
|
||||
# /model/info just shows models user has access to
|
||||
|
@ -1406,7 +1408,7 @@ async def user_api_key_auth(
|
|||
"/global/predict/spend/logs",
|
||||
"/global/activity",
|
||||
"/health/services",
|
||||
]
|
||||
] + LiteLLMRoutes.info_routes.value
|
||||
# check if the current route startswith any of the allowed routes
|
||||
if (
|
||||
route is not None
|
||||
|
@ -9164,6 +9166,7 @@ async def new_user(data: NewUserRequest):
|
|||
data_json["table_name"] = (
|
||||
"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)
|
||||
|
||||
# Admin UI Logic
|
||||
|
@ -12765,7 +12768,10 @@ async def login(request: Request):
|
|||
_password = getattr(_user_row, "password", "unknown")
|
||||
|
||||
# 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:
|
||||
response = await generate_key_helper_fn(
|
||||
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)
|
||||
def get_image():
|
||||
"""Get logo to show on admin UI"""
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
"use client";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Card, Title, Text, TextInput, Callout, Button, Grid, Col } from "@tremor/react";
|
||||
import { RiAlarmWarningLine, RiCheckboxCircleLine } from '@remixicon/react';
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
TextInput,
|
||||
Callout,
|
||||
Button,
|
||||
Grid,
|
||||
Col,
|
||||
} from "@tremor/react";
|
||||
import { RiAlarmWarningLine, RiCheckboxCircleLine } from "@remixicon/react";
|
||||
import {
|
||||
invitationClaimCall,
|
||||
userUpdateUserCall,
|
||||
getOnboardingCredentials,
|
||||
claimOnboardingToken,
|
||||
} from "@/components/networking";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { Form, Button as Button2, message } from "antd";
|
||||
|
@ -18,6 +28,7 @@ export default function Onboarding() {
|
|||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [defaultUserEmail, setDefaultUserEmail] = useState<string>("");
|
||||
const [userEmail, setUserEmail] = useState<string>("");
|
||||
const [userID, setUserID] = useState<string | null>(null);
|
||||
const [loginUrl, setLoginUrl] = useState<string>("");
|
||||
const [jwtToken, setJwtToken] = useState<string>("");
|
||||
|
||||
|
@ -30,7 +41,6 @@ export default function Onboarding() {
|
|||
console.log("login_url:", login_url);
|
||||
setLoginUrl(login_url);
|
||||
|
||||
|
||||
const token = data.token;
|
||||
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||
setJwtToken(token);
|
||||
|
@ -42,31 +52,44 @@ export default function Onboarding() {
|
|||
const user_email = decoded.user_email;
|
||||
setUserEmail(user_email);
|
||||
|
||||
const user_id = decoded.user_id;
|
||||
setUserID(user_id);
|
||||
});
|
||||
|
||||
}, [inviteID]);
|
||||
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
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/";
|
||||
const user_id = data.data?.user_id || data.user_id;
|
||||
litellm_dashboard_ui += "?userID=" + user_id + "&token=" + jwtToken;
|
||||
console.log("redirecting to:", litellm_dashboard_ui);
|
||||
|
||||
window.location.href = litellm_dashboard_ui;
|
||||
|
||||
});
|
||||
|
||||
// redirect to login page
|
||||
|
||||
};
|
||||
return (
|
||||
<div className="mx-auto max-w-md mt-10">
|
||||
|
@ -82,19 +105,15 @@ export default function Onboarding() {
|
|||
color="sky"
|
||||
>
|
||||
<Grid numItems={2} className="flex justify-between items-center">
|
||||
<Col>
|
||||
SSO is under the Enterprise Tirer.
|
||||
</Col>
|
||||
<Col>SSO is under the Enterprise Tirer.</Col>
|
||||
|
||||
<Col>
|
||||
<Button variant="primary" className="mb-2">
|
||||
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
||||
Get Free Trial
|
||||
</a>
|
||||
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
</Grid>
|
||||
</Callout>
|
||||
|
||||
|
@ -104,10 +123,7 @@ export default function Onboarding() {
|
|||
onFinish={handleSubmit}
|
||||
>
|
||||
<>
|
||||
<Form.Item
|
||||
label="Email Address"
|
||||
name="user_email"
|
||||
>
|
||||
<Form.Item label="Email Address" name="user_email">
|
||||
<TextInput
|
||||
type="email"
|
||||
disabled={true}
|
||||
|
@ -120,7 +136,9 @@ export default function Onboarding() {
|
|||
<Form.Item
|
||||
label="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"
|
||||
>
|
||||
<TextInput placeholder="" type="password" className="max-w-md" />
|
||||
|
|
|
@ -33,6 +33,8 @@ import {
|
|||
Divider,
|
||||
} from "@tremor/react";
|
||||
import { PencilAltIcon } from "@heroicons/react/outline";
|
||||
import OnboardingModal from "./onboarding_link";
|
||||
import { InvitationLink } from "./onboarding_link";
|
||||
interface AdminPanelProps {
|
||||
searchParams: any;
|
||||
accessToken: string | null;
|
||||
|
@ -40,17 +42,6 @@ interface AdminPanelProps {
|
|||
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 {
|
||||
userUpdateUserCall,
|
||||
Member,
|
||||
|
@ -84,11 +75,15 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
useState(false);
|
||||
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 [baseUrl, setBaseUrl] = useState(isLocal ? "http://localhost:4000" : "");
|
||||
|
||||
const [baseUrl, setBaseUrl] = useState(
|
||||
isLocal ? "http://localhost:4000" : ""
|
||||
);
|
||||
|
||||
let nonSssoUrl;
|
||||
try {
|
||||
|
@ -346,7 +341,6 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
setIsInvitationLinkModalVisible(true);
|
||||
});
|
||||
|
||||
|
||||
const foundIndex = admins.findIndex((user) => {
|
||||
console.log(
|
||||
`user.user_id=${user.user_id}; response.user_id=${response.user_id}`
|
||||
|
@ -462,7 +456,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
? member["user_id"]
|
||||
: null}
|
||||
</TableCell>
|
||||
<TableCell> {possibleUIRoles?.[member?.user_role]?.ui_label || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{" "}
|
||||
{possibleUIRoles?.[member?.user_role]?.ui_label ||
|
||||
"-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
|
@ -509,39 +507,12 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
>
|
||||
{addMemberForm(handleAdminCreate)}
|
||||
</Modal>
|
||||
<Modal
|
||||
title="Invitation Link"
|
||||
visible={isInvitationLinkModalVisible}
|
||||
width={800}
|
||||
footer={null}
|
||||
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>
|
||||
<OnboardingModal
|
||||
isInvitationLinkModalVisible={isInvitationLinkModalVisible}
|
||||
setIsInvitationLinkModalVisible={setIsInvitationLinkModalVisible}
|
||||
baseUrl={baseUrl}
|
||||
invitationLinkData={invitationLinkData}
|
||||
/>
|
||||
<Button
|
||||
className="mb-5"
|
||||
onClick={() => setIsAddMemberModalVisible(true)}
|
||||
|
|
|
@ -1,25 +1,51 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Modal, Form, Input, message, Select, InputNumber } from "antd";
|
||||
import { Button as Button2, Text, TextInput } from "@tremor/react";
|
||||
import { userCreateCall, modelAvailableCall } from "./networking";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
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;
|
||||
|
||||
interface CreateuserProps {
|
||||
userID: string;
|
||||
accessToken: string;
|
||||
teams: any[] | null;
|
||||
possibleUIRoles: null | Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
const Createuser: React.FC<CreateuserProps> = ({
|
||||
userID,
|
||||
accessToken,
|
||||
teams,
|
||||
possibleUIRoles,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [apiuser, setApiuser] = useState<string | null>(null);
|
||||
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
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
|
@ -48,6 +74,14 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
|
||||
fetchData(); // Call the function to fetch model data when the component mounts
|
||||
}, []); // Empty dependency array to run only once
|
||||
|
||||
useEffect(() => {
|
||||
if (router) {
|
||||
const { protocol, host } = window.location;
|
||||
const baseUrl = `${protocol}/${host}`;
|
||||
setBaseUrl(baseUrl);
|
||||
}
|
||||
}, [router]);
|
||||
const handleOk = () => {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
|
@ -67,6 +101,11 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
const response = await userCreateCall(accessToken, null, formValues);
|
||||
console.log("user create Response:", response);
|
||||
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");
|
||||
form.resetFields();
|
||||
localStorage.removeItem("userData" + userID);
|
||||
|
@ -88,12 +127,7 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Text className="mb-1">
|
||||
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>
|
||||
<Text className="mb-1">Create a User who can own keys</Text>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleCreate}
|
||||
|
@ -104,6 +138,26 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
<Form.Item label="User Email" name="user_email">
|
||||
<TextInput placeholder="" />
|
||||
</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">
|
||||
<Select placeholder="Select Team ID" style={{ width: "100%" }}>
|
||||
{teams ? (
|
||||
|
@ -119,6 +173,7 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Metadata" name="metadata">
|
||||
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
|
||||
</Form.Item>
|
||||
|
@ -128,25 +183,12 @@ const Createuser: React.FC<CreateuserProps> = ({
|
|||
</Form>
|
||||
</Modal>
|
||||
{apiuser && (
|
||||
<Modal
|
||||
title="User Created Successfully"
|
||||
visible={isModalVisible}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
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>
|
||||
<OnboardingModal
|
||||
isInvitationLinkModalVisible={isInvitationLinkModalVisible}
|
||||
setIsInvitationLinkModalVisible={setIsInvitationLinkModalVisible}
|
||||
baseUrl={baseUrl}
|
||||
invitationLinkData={invitationLinkData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -522,6 +522,12 @@ export const userInfoCall = async (
|
|||
if (userRole == "App User" && 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);
|
||||
if (viewAll && page_size && page != null && page != undefined) {
|
||||
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) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/onboarding/get_token` : `/onboarding/get_token`;
|
||||
url += `?invite_link=${inviteUUID}`
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/onboarding/get_token`
|
||||
: `/onboarding/get_token`;
|
||||
url += `?invite_link=${inviteUUID}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
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 (
|
||||
accessToken: String,
|
||||
userID: String,
|
||||
|
@ -1024,15 +1068,12 @@ export const tagsSpendLogsCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const allTagNamesCall = async (
|
||||
accessToken: String,
|
||||
) => {
|
||||
export const allTagNamesCall = async (accessToken: String) => {
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/global/spend/all_tag_names`
|
||||
: `/global/spend/all_tag_names`;
|
||||
|
||||
|
||||
console.log("in global/spend/all_tag_names call", url);
|
||||
const response = await fetch(`${url}`, {
|
||||
method: "GET",
|
||||
|
@ -1055,16 +1096,12 @@ export const allTagNamesCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
export const allEndUsersCall = async (
|
||||
accessToken: String,
|
||||
) => {
|
||||
export const allEndUsersCall = async (accessToken: String) => {
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/global/all_end_users`
|
||||
: `/global/all_end_users`;
|
||||
|
||||
|
||||
console.log("in global/all_end_users call", url);
|
||||
const response = await fetch(`${url}`, {
|
||||
method: "GET",
|
||||
|
@ -1087,7 +1124,6 @@ export const allEndUsersCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
export const userSpendLogsCall = async (
|
||||
accessToken: String,
|
||||
token: String,
|
||||
|
@ -1377,13 +1413,11 @@ export const adminGlobalActivityPerModel = async (
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const adminGlobalActivityExceptions = async (
|
||||
accessToken: String,
|
||||
startTime: String | undefined,
|
||||
endTime: String | undefined,
|
||||
modelGroup: String,
|
||||
modelGroup: String
|
||||
) => {
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
|
@ -1429,7 +1463,7 @@ export const adminGlobalActivityExceptionsPerDeployment = async (
|
|||
accessToken: String,
|
||||
startTime: String | undefined,
|
||||
endTime: String | undefined,
|
||||
modelGroup: String,
|
||||
modelGroup: String
|
||||
) => {
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
|
@ -1666,9 +1700,7 @@ export const userGetAllUsersCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const getPossibleUserRoles = async (
|
||||
accessToken: String,
|
||||
) => {
|
||||
export const getPossibleUserRoles = async (accessToken: String) => {
|
||||
try {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/user/available_roles`
|
||||
|
|
86
ui/litellm-dashboard/src/components/onboarding_link.tsx
Normal file
86
ui/litellm-dashboard/src/components/onboarding_link.tsx
Normal 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;
|
|
@ -101,7 +101,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
return "App User";
|
||||
case "internal_user":
|
||||
return "Internal User";
|
||||
case "internal_viewer":
|
||||
case "internal_user_viewer":
|
||||
return "Internal Viewer";
|
||||
default:
|
||||
return "Unknown Role";
|
||||
|
|
|
@ -25,11 +25,13 @@ import {
|
|||
TextInput,
|
||||
} from "@tremor/react";
|
||||
|
||||
import {
|
||||
message,
|
||||
} from "antd";
|
||||
import { message } from "antd";
|
||||
|
||||
import { userInfoCall, userUpdateUserCall, getPossibleUserRoles } from "./networking";
|
||||
import {
|
||||
userInfoCall,
|
||||
userUpdateUserCall,
|
||||
getPossibleUserRoles,
|
||||
} from "./networking";
|
||||
import { Badge, BadgeDelta, Button } from "@tremor/react";
|
||||
import RequestAccess from "./request_model_access";
|
||||
import CreateUser from "./create_user_button";
|
||||
|
@ -67,7 +69,9 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
const [selectedItem, setSelectedItem] = useState<null | any>(null);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
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 handleEditCancel = async () => {
|
||||
|
@ -119,13 +123,11 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
|
||||
const availableUserRoles = await getPossibleUserRoles(accessToken);
|
||||
setPossibleUIRoles(availableUserRoles);
|
||||
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the model data", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (accessToken && token && userRole && userID) {
|
||||
fetchData();
|
||||
}
|
||||
|
@ -174,10 +176,14 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
<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">
|
||||
<div className="mb-4 mt-1">
|
||||
</div>
|
||||
<div className="mb-4 mt-1"></div>
|
||||
<TabGroup>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
|
@ -218,7 +224,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
user.key_aliases.filter(
|
||||
(key: any) => key !== null
|
||||
).length
|
||||
|
||||
}
|
||||
Keys
|
||||
</Badge>
|
||||
|
@ -236,20 +241,21 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
</Grid>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
<Icon icon={PencilAltIcon} onClick= {() => {
|
||||
setSelectedUser(user)
|
||||
setEditModalVisible(true)
|
||||
}}>View Keys</Icon>
|
||||
{/*
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
onClick={() => {
|
||||
setSelectedUser(user);
|
||||
setEditModalVisible(true);
|
||||
}}
|
||||
>
|
||||
View Keys
|
||||
</Icon>
|
||||
{/*
|
||||
<Icon icon={TrashIcon} onClick= {() => {
|
||||
setOpenDialogId(user.user_id)
|
||||
setSelectedItem(user)
|
||||
}}>View Keys</Icon> */}
|
||||
|
||||
</TableCell>
|
||||
|
||||
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue