mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 13:27:22 +00:00
feat: allow admins to delete users
This commit is contained in:
@@ -4,6 +4,7 @@ import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -91,6 +92,75 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/list",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
users: UsersSchema.pick({
|
||||||
|
username: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true,
|
||||||
|
id: true
|
||||||
|
}).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
const users = await server.services.user.listUsers();
|
||||||
|
|
||||||
|
return {
|
||||||
|
users
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:userId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
users: UsersSchema.pick({
|
||||||
|
username: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
email: true,
|
||||||
|
id: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const users = await server.services.user.deleteUser(req.params.userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
users
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/me/project-favorites",
|
url: "/me/project-favorites",
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const user = await server.services.user.deleteMe(req.permission.id);
|
const user = await server.services.user.deleteUser(req.permission.id);
|
||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export const userServiceFactory = ({
|
|||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMe = async (userId: string) => {
|
const deleteUser = async (userId: string) => {
|
||||||
const user = await userDAL.deleteById(userId);
|
const user = await userDAL.deleteById(userId);
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
@@ -295,19 +295,24 @@ export const userServiceFactory = ({
|
|||||||
return updatedOrgMembership.projectFavorites;
|
return updatedOrgMembership.projectFavorites;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listUsers = async () => {
|
||||||
|
return userDAL.find({});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendEmailVerificationCode,
|
sendEmailVerificationCode,
|
||||||
verifyEmailVerificationCode,
|
verifyEmailVerificationCode,
|
||||||
toggleUserMfa,
|
toggleUserMfa,
|
||||||
updateUserName,
|
updateUserName,
|
||||||
updateAuthMethods,
|
updateAuthMethods,
|
||||||
deleteMe,
|
deleteUser,
|
||||||
getMe,
|
getMe,
|
||||||
createUserAction,
|
createUserAction,
|
||||||
getUserAction,
|
getUserAction,
|
||||||
unlockUser,
|
unlockUser,
|
||||||
getUserPrivateKey,
|
getUserPrivateKey,
|
||||||
getUserProjectFavorites,
|
getUserProjectFavorites,
|
||||||
updateUserProjectFavorites
|
updateUserProjectFavorites,
|
||||||
|
listUsers
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
useAddUserToWsE2EE,
|
useAddUserToWsE2EE,
|
||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
|
useDeleteUser,
|
||||||
useSendEmailVerificationCode,
|
useSendEmailVerificationCode,
|
||||||
useVerifyEmailVerificationCode
|
useVerifyEmailVerificationCode
|
||||||
} from "./mutation";
|
} from "./mutation";
|
||||||
@@ -10,7 +11,6 @@ export {
|
|||||||
useCreateAPIKey,
|
useCreateAPIKey,
|
||||||
useDeleteAPIKey,
|
useDeleteAPIKey,
|
||||||
useDeleteOrgMembership,
|
useDeleteOrgMembership,
|
||||||
useDeleteUser,
|
|
||||||
useGetMyAPIKeys,
|
useGetMyAPIKeys,
|
||||||
useGetMyAPIKeysV2,
|
useGetMyAPIKeysV2,
|
||||||
useGetMyIp,
|
useGetMyIp,
|
||||||
@@ -19,6 +19,7 @@ export {
|
|||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
|
useListUsers,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction,
|
||||||
useRevokeMySessions,
|
useRevokeMySessions,
|
||||||
|
|||||||
@@ -112,3 +112,17 @@ export const useUpdateUserProjectFavorites = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDeleteUser = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (userId: string) => {
|
||||||
|
await apiRequest.delete(`/api/v1/user/${userId}`);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(userKeys.listUsers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const userKeys = {
|
|||||||
myAPIKeys: ["api-keys"] as const,
|
myAPIKeys: ["api-keys"] as const,
|
||||||
myAPIKeysV2: ["api-keys-v2"] as const,
|
myAPIKeysV2: ["api-keys-v2"] as const,
|
||||||
mySessions: ["sessions"] as const,
|
mySessions: ["sessions"] as const,
|
||||||
|
listUsers: ["user-list"] as const,
|
||||||
|
|
||||||
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
||||||
};
|
};
|
||||||
@@ -40,6 +41,14 @@ export const fetchUserDetails = async () => {
|
|||||||
|
|
||||||
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
|
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
|
||||||
|
|
||||||
|
export const fetchUsersList = async () => {
|
||||||
|
const { data } = await apiRequest.get<{ users: User[] }>("/api/v1/user/list");
|
||||||
|
|
||||||
|
return data.users;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListUsers = () => useQuery(userKeys.listUsers, fetchUsersList);
|
||||||
|
|
||||||
export const useDeleteUser = () => {
|
export const useDeleteUser = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api";
|
|||||||
|
|
||||||
import { AuthPanel } from "./AuthPanel";
|
import { AuthPanel } from "./AuthPanel";
|
||||||
import { RateLimitPanel } from "./RateLimitPanel";
|
import { RateLimitPanel } from "./RateLimitPanel";
|
||||||
|
import { UserPanel } from "./UserPanel";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
Auth = "auth",
|
Auth = "auth",
|
||||||
RateLimit = "rate-limit"
|
RateLimit = "rate-limit",
|
||||||
|
Users = "users"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SignUpModes {
|
enum SignUpModes {
|
||||||
@@ -135,6 +137,7 @@ export const AdminDashboardPage = () => {
|
|||||||
<Tab value={TabSections.Settings}>General</Tab>
|
<Tab value={TabSections.Settings}>General</Tab>
|
||||||
<Tab value={TabSections.Auth}>Authentication</Tab>
|
<Tab value={TabSections.Auth}>Authentication</Tab>
|
||||||
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
||||||
|
<Tab value={TabSections.Users}>Users</Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Settings}>
|
<TabPanel value={TabSections.Settings}>
|
||||||
@@ -320,6 +323,9 @@ export const AdminDashboardPage = () => {
|
|||||||
<TabPanel value={TabSections.RateLimit}>
|
<TabPanel value={TabSections.RateLimit}>
|
||||||
<RateLimitPanel />
|
<RateLimitPanel />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Users}>
|
||||||
|
<UserPanel />
|
||||||
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { faMagnifyingGlass, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useUser } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteUser, useListUsers } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const UserPanel = () => {
|
||||||
|
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"removeUser"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const userId = user?.id || "";
|
||||||
|
const { data: users, isLoading } = useListUsers();
|
||||||
|
const { mutateAsync: deleteUser } = useDeleteUser();
|
||||||
|
|
||||||
|
const filterdUsers = useMemo(
|
||||||
|
() =>
|
||||||
|
users?.filter(
|
||||||
|
({ firstName, lastName, username, email }) =>
|
||||||
|
firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
|
||||||
|
lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
|
||||||
|
username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) ||
|
||||||
|
email?.toLowerCase().includes(searchMemberFilter.toLowerCase())
|
||||||
|
),
|
||||||
|
[users, searchMemberFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveUser = async () => {
|
||||||
|
const { id } = popUp?.removeUser?.data as { id: string; username: string };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteUser(id);
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully deleted user"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Error deleting user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpClose("removeUser");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={searchMemberFilter}
|
||||||
|
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search members..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Username</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} innerKey="users" />}
|
||||||
|
{!isLoading &&
|
||||||
|
filterdUsers?.map(({ username, email, firstName, lastName, id }) => {
|
||||||
|
const name = firstName || lastName ? `${firstName} ${lastName}` : "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`user-${id}`} className="w-full">
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{email}</Td>
|
||||||
|
<Td>
|
||||||
|
{userId !== id && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<IconButton
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
|
isDisabled={userId === id}
|
||||||
|
onClick={() => handlePopUpOpen("removeUser", { username, id })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && filterdUsers?.length === 0 && (
|
||||||
|
<EmptyState title="No users found" icon={faUsers} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeUser.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title={`Are you sure you want to delete User with username ${
|
||||||
|
(popUp?.removeUser?.data as { id: string; username: string })?.username || ""
|
||||||
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)}
|
||||||
|
onDeleteApproved={handleRemoveUser}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user