diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index d3c0db242f..3510179c96 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -4,6 +4,7 @@ import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; 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 { 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({ method: "GET", url: "/me/project-favorites", diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 21dd32021a..01c7eda6d8 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -297,7 +297,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), 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 }; } }); diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index 5f01066044..de10baf8ff 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -201,7 +201,7 @@ export const userServiceFactory = ({ return user; }; - const deleteMe = async (userId: string) => { + const deleteUser = async (userId: string) => { const user = await userDAL.deleteById(userId); return user; }; @@ -295,19 +295,24 @@ export const userServiceFactory = ({ return updatedOrgMembership.projectFavorites; }; + const listUsers = async () => { + return userDAL.find({}); + }; + return { sendEmailVerificationCode, verifyEmailVerificationCode, toggleUserMfa, updateUserName, updateAuthMethods, - deleteMe, + deleteUser, getMe, createUserAction, getUserAction, unlockUser, getUserPrivateKey, getUserProjectFavorites, - updateUserProjectFavorites + updateUserProjectFavorites, + listUsers }; }; diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index a8ad89f4cf..87cf7c78f3 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -1,6 +1,7 @@ export { useAddUserToWsE2EE, useAddUserToWsNonE2EE, + useDeleteUser, useSendEmailVerificationCode, useVerifyEmailVerificationCode } from "./mutation"; @@ -10,7 +11,6 @@ export { useCreateAPIKey, useDeleteAPIKey, useDeleteOrgMembership, - useDeleteUser, useGetMyAPIKeys, useGetMyAPIKeysV2, useGetMyIp, @@ -19,6 +19,7 @@ export { useGetOrgUsers, useGetUser, useGetUserAction, + useListUsers, useLogoutUser, useRegisterUserAction, useRevokeMySessions, diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx index 26e932ac6d..9aed160daa 100644 --- a/frontend/src/hooks/api/users/mutation.tsx +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -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); + } + }); +}; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 1a49e8d7c5..962961bc29 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -28,6 +28,7 @@ export const userKeys = { myAPIKeys: ["api-keys"] as const, myAPIKeysV2: ["api-keys-v2"] as const, mySessions: ["sessions"] as const, + listUsers: ["user-list"] 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 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 = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 42d3cdc1b4..715bb2e479 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -26,11 +26,13 @@ import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api"; import { AuthPanel } from "./AuthPanel"; import { RateLimitPanel } from "./RateLimitPanel"; +import { UserPanel } from "./UserPanel"; enum TabSections { Settings = "settings", Auth = "auth", - RateLimit = "rate-limit" + RateLimit = "rate-limit", + Users = "users" } enum SignUpModes { @@ -135,6 +137,7 @@ export const AdminDashboardPage = () => { General Authentication Rate Limit + Users @@ -320,6 +323,9 @@ export const AdminDashboardPage = () => { + + + )} diff --git a/frontend/src/views/admin/DashboardPage/UserPanel.tsx b/frontend/src/views/admin/DashboardPage/UserPanel.tsx new file mode 100644 index 0000000000..b79eb4981c --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/UserPanel.tsx @@ -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 ( +
+
+

Users

+
+ setSearchMemberFilter(e.target.value)} + leftIcon={} + placeholder="Search members..." + /> + +
+ + + + + + + + + + {isLoading && } + {!isLoading && + filterdUsers?.map(({ username, email, firstName, lastName, id }) => { + const name = firstName || lastName ? `${firstName} ${lastName}` : "-"; + + return ( + + + + + + ); + })} + +
NameUsername +
{name}{email} + {userId !== id && ( +
+ handlePopUpOpen("removeUser", { username, id })} + > + + +
+ )} +
+ {!isLoading && filterdUsers?.length === 0 && ( + + )} +
+
+ handlePopUpToggle("removeUser", isOpen)} + onDeleteApproved={handleRemoveUser} + /> +
+ ); +};