admin can reset password for user (#11618)

This commit is contained in:
tanjiro
2025-06-11 08:55:36 -07:00
committed by GitHub
parent 0d766808e9
commit c002e4cd4e
5 changed files with 155 additions and 48 deletions
@@ -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>
);
}