mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-03 18:23:40 +00:00
admin can reset password for user (#11618)
This commit is contained in:
@@ -29,6 +29,7 @@ export default function Onboarding() {
|
||||
const searchParams = useSearchParams()!;
|
||||
const token = getCookie('token');
|
||||
const inviteID = searchParams.get("invitation_id");
|
||||
const action = searchParams.get("action");
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [defaultUserEmail, setDefaultUserEmail] = useState<string>("");
|
||||
const [userEmail, setUserEmail] = useState<string>("");
|
||||
@@ -117,27 +118,29 @@ export default function Onboarding() {
|
||||
<div className="mx-auto w-full max-w-md mt-10">
|
||||
<Card>
|
||||
<Title className="text-sm mb-5 text-center">🚅 LiteLLM</Title>
|
||||
<Title className="text-xl">Sign up</Title>
|
||||
<Text>Claim your user account to login to Admin UI.</Text>
|
||||
<Title className="text-xl">{action === "reset_password" ? "Reset Password" : "Sign up"}</Title>
|
||||
<Text>{action === "reset_password" ? "Reset your password to access Admin UI." : "Claim your user account to login to Admin UI."}</Text>
|
||||
|
||||
<Callout
|
||||
className="mt-4"
|
||||
title="SSO"
|
||||
icon={RiCheckboxCircleLine}
|
||||
color="sky"
|
||||
>
|
||||
<Grid numItems={2} className="flex justify-between items-center">
|
||||
<Col>SSO is under the Enterprise Tier.</Col>
|
||||
{action !== "reset_password" && (
|
||||
<Callout
|
||||
className="mt-4"
|
||||
title="SSO"
|
||||
icon={RiCheckboxCircleLine}
|
||||
color="sky"
|
||||
>
|
||||
<Grid numItems={2} className="flex justify-between items-center">
|
||||
<Col>SSO is under the Enterprise Tier.</Col>
|
||||
|
||||
<Col>
|
||||
<Button variant="primary" className="mb-2">
|
||||
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
||||
Get Free Trial
|
||||
</a>
|
||||
</Button>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Callout>
|
||||
<Col>
|
||||
<Button variant="primary" className="mb-2">
|
||||
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
||||
Get Free Trial
|
||||
</a>
|
||||
</Button>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<Form
|
||||
className="mt-10 mb-5 mx-auto"
|
||||
@@ -161,14 +164,14 @@ export default function Onboarding() {
|
||||
rules={[
|
||||
{ required: true, message: "password required to sign up" },
|
||||
]}
|
||||
help="Create a password for your account"
|
||||
help={action === "reset_password" ? "Enter your new password" : "Create a password for your account"}
|
||||
>
|
||||
<TextInput placeholder="" type="password" className="max-w-md" />
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
<div className="mt-10">
|
||||
<Button2 htmlType="submit">Sign Up</Button2>
|
||||
<Button2 htmlType="submit">{action === "reset_password" ? "Reset Password" : "Sign Up"}</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
@@ -23,6 +23,7 @@ interface OnboardingProps {
|
||||
>;
|
||||
baseUrl: string;
|
||||
invitationLinkData: InvitationLink | null;
|
||||
modalType?: "invitation" | "resetPassword";
|
||||
}
|
||||
|
||||
export default function OnboardingModal({
|
||||
@@ -30,6 +31,7 @@ export default function OnboardingModal({
|
||||
setIsInvitationLinkModalVisible,
|
||||
baseUrl,
|
||||
invitationLinkData,
|
||||
modalType = "invitation",
|
||||
}: OnboardingProps) {
|
||||
const { Title, Paragraph } = Typography;
|
||||
const handleInvitationOk = () => {
|
||||
@@ -41,6 +43,9 @@ export default function OnboardingModal({
|
||||
};
|
||||
|
||||
const getInvitationUrl = () => {
|
||||
if (!baseUrl) {
|
||||
return "";
|
||||
}
|
||||
const baseUrlObj = new URL(baseUrl);
|
||||
const basePath = baseUrlObj.pathname; // This will be "/litellm" or ""
|
||||
const path = basePath && basePath !== "/" ? `${basePath}/ui` : 'ui';
|
||||
@@ -48,13 +53,17 @@ export default function OnboardingModal({
|
||||
if (invitationLinkData?.has_user_setup_sso) {
|
||||
return new URL(path, baseUrl).toString();
|
||||
}
|
||||
const url = new URL(`${path}?invitation_id=${invitationLinkData?.id}`, baseUrl).toString();
|
||||
let urlPath = `${path}?invitation_id=${invitationLinkData?.id}`;
|
||||
if (modalType === "resetPassword") {
|
||||
urlPath += "&action=reset_password";
|
||||
}
|
||||
const url = new URL(urlPath, baseUrl).toString();
|
||||
return url;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Invitation Link"
|
||||
title={modalType === "invitation" ? "Invitation Link" : "Reset Password Link"}
|
||||
visible={isInvitationLinkModalVisible}
|
||||
width={800}
|
||||
footer={null}
|
||||
@@ -62,14 +71,16 @@ export default function OnboardingModal({
|
||||
onCancel={handleInvitationCancel}
|
||||
>
|
||||
<Paragraph>
|
||||
Copy and send the generated link to onboard this user to the proxy.
|
||||
{modalType === "invitation"
|
||||
? "Copy and send the generated link to onboard this user to the proxy."
|
||||
: "Copy and send the generated link to the user to reset their password."}
|
||||
</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>{modalType === "invitation" ? "Invitation Link" : "Reset Password Link"}</Text>
|
||||
<Text>
|
||||
<Text>{getInvitationUrl()}</Text>
|
||||
</Text>
|
||||
@@ -79,7 +90,11 @@ export default function OnboardingModal({
|
||||
text={getInvitationUrl()}
|
||||
onCopy={() => message.success("Copied!")}
|
||||
>
|
||||
<Button variant="primary">Copy invitation link</Button>
|
||||
<Button variant="primary">
|
||||
{modalType === "invitation"
|
||||
? "Copy invitation link"
|
||||
: "Copy password reset link"}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -17,10 +17,14 @@ import {
|
||||
getPossibleUserRoles,
|
||||
userListCall,
|
||||
UserListResponse,
|
||||
invitationCreateCall,
|
||||
getProxyBaseUrl,
|
||||
} from "./networking";
|
||||
import { Button } from "@tremor/react";
|
||||
import CreateUser from "./create_user_button";
|
||||
import EditUserModal from "./edit_user";
|
||||
import OnboardingModal from "./onboarding_link";
|
||||
import { InvitationLink } from "./onboarding_link";
|
||||
|
||||
import { userDeleteCall } from "./networking";
|
||||
import { columns } from "./view_users/columns";
|
||||
@@ -88,6 +92,12 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||
const [filters, setFilters] = useState<FilterState>(initialFilters);
|
||||
const [debouncedFilters, setDebouncedFilters, debouncer] = useDebouncedState(filters, { wait: 300 })
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] =
|
||||
useState(false);
|
||||
const [invitationLinkData, setInvitationLinkData] =
|
||||
useState<InvitationLink | null>(null);
|
||||
const [baseUrl, setBaseUrl] = useState<string | null>(null);
|
||||
|
||||
const handleDelete = (userId: string) => {
|
||||
setUserToDelete(userId);
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -99,6 +109,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||
}
|
||||
}, [debouncer])
|
||||
|
||||
useEffect(() => {
|
||||
setBaseUrl(getProxyBaseUrl());
|
||||
}, []);
|
||||
|
||||
const updateFilters = (update: Partial<FilterState>) => {
|
||||
setFilters((previousFilters) => {
|
||||
const newFilters = {...previousFilters, ...update };
|
||||
@@ -111,6 +125,20 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||
updateFilters({ sort_by: sortBy, sort_order: sortOrder });
|
||||
};
|
||||
|
||||
const handleResetPassword = async (userId: string) => {
|
||||
if (!accessToken) {
|
||||
message.error("Access token not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
message.success("Generating password reset link...");
|
||||
const data = await invitationCreateCall(accessToken, userId);
|
||||
setInvitationLinkData(data);
|
||||
setIsInvitationLinkModalVisible(true);
|
||||
} catch (error) {
|
||||
message.error("Failed to generate password reset link");
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (userToDelete && accessToken) {
|
||||
@@ -226,7 +254,8 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||
setSelectedUser(user);
|
||||
setEditModalVisible(true);
|
||||
},
|
||||
handleDelete
|
||||
handleDelete,
|
||||
handleResetPassword
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -523,6 +552,14 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<OnboardingModal
|
||||
isInvitationLinkModalVisible={isInvitationLinkModalVisible}
|
||||
setIsInvitationLinkModalVisible={setIsInvitationLinkModalVisible}
|
||||
baseUrl={baseUrl || ""}
|
||||
invitationLinkData={invitationLinkData}
|
||||
modalType="resetPassword"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,13 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Grid, Icon } from "@tremor/react";
|
||||
import { Tooltip } from "antd";
|
||||
import { UserInfo } from "./types";
|
||||
import { PencilAltIcon, TrashIcon, InformationCircleIcon } from "@heroicons/react/outline";
|
||||
import { PencilAltIcon, TrashIcon, InformationCircleIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||
|
||||
export const columns = (
|
||||
possibleUIRoles: Record<string, Record<string, string>>,
|
||||
handleEdit: (user: UserInfo) => void,
|
||||
handleDelete: (userId: string) => void,
|
||||
handleResetPassword: (userId: string) => void
|
||||
): ColumnDef<UserInfo>[] => [
|
||||
{
|
||||
header: "User ID",
|
||||
@@ -110,16 +111,27 @@ export const columns = (
|
||||
header: "",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
size="sm"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
/>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => handleDelete(row.original.user_id)}
|
||||
/>
|
||||
<Tooltip title="Edit user" zIndex={9999}>
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
size="sm"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete user" zIndex={9999}>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => handleDelete(row.original.user_id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reset Password" zIndex={9999}>
|
||||
<Icon
|
||||
icon={RefreshIcon}
|
||||
size="sm"
|
||||
onClick={() => handleResetPassword(row.original.user_id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -13,11 +13,12 @@ import {
|
||||
Title,
|
||||
Badge,
|
||||
} from "@tremor/react";
|
||||
import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { userInfoCall, userDeleteCall, userUpdateUserCall, modelAvailableCall } from "../networking";
|
||||
import { ArrowLeftIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||
import { userInfoCall, userDeleteCall, userUpdateUserCall, modelAvailableCall, invitationCreateCall, getProxyBaseUrl } from "../networking";
|
||||
import { message } from "antd";
|
||||
import { rolesWithWriteAccess } from '../../utils/roles';
|
||||
import { UserEditView } from "../user_edit_view";
|
||||
import OnboardingModal, { InvitationLink } from "../onboarding_link";
|
||||
|
||||
interface UserInfoViewProps {
|
||||
userId: string;
|
||||
@@ -51,6 +52,13 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false);
|
||||
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null);
|
||||
const [baseUrl, setBaseUrl] = useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setBaseUrl(getProxyBaseUrl());
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(`userId: ${userId}, userRole: ${userRole}, accessToken: ${accessToken}`)
|
||||
@@ -75,6 +83,21 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
||||
fetchData();
|
||||
}, [accessToken, userId, userRole]);
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
if (!accessToken) {
|
||||
message.error("Access token not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
message.success("Generating password reset link...");
|
||||
const data = await invitationCreateCall(accessToken, userId);
|
||||
setInvitationLinkData(data);
|
||||
setIsInvitationLinkModalVisible(true);
|
||||
} catch (error) {
|
||||
message.error("Failed to generate password reset link");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
@@ -164,14 +187,24 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
||||
<Text className="text-gray-500 font-mono">{userData.user_id}</Text>
|
||||
</div>
|
||||
{userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<Button
|
||||
icon={TrashIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center"
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
icon={RefreshIcon}
|
||||
variant="secondary"
|
||||
onClick={handleResetPassword}
|
||||
className="flex items-center"
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
<Button
|
||||
icon={TrashIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center"
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -381,6 +414,13 @@ export default function UserInfoView({ userId, onClose, accessToken, userRole, o
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
<OnboardingModal
|
||||
isInvitationLinkModalVisible={isInvitationLinkModalVisible}
|
||||
setIsInvitationLinkModalVisible={setIsInvitationLinkModalVisible}
|
||||
baseUrl={baseUrl || ""}
|
||||
invitationLinkData={invitationLinkData}
|
||||
modalType="resetPassword"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user