mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 20:27:12 +00:00
Merge pull request #2086 from Infisical/feat/allow-admins-to-delete-users
feat: allow admins to delete users
This commit is contained in:
@@ -83,6 +83,82 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/user-management/users",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
searchTerm: z.string().default(""),
|
||||||
|
offset: z.coerce.number().default(0),
|
||||||
|
limit: z.coerce.number().max(100).default(20)
|
||||||
|
}),
|
||||||
|
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 (req) => {
|
||||||
|
const users = await server.services.superAdmin.getUsers({
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
users
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/user-management/users/: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.superAdmin.deleteUser(req.params.userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
users
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/signup",
|
url: "/signup",
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AuthMethod } from "../auth/auth-type";
|
|||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||||
import { LoginMethod, TAdminSignUpDTO } from "./super-admin-types";
|
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
|
||||||
|
|
||||||
type TSuperAdminServiceFactoryDep = {
|
type TSuperAdminServiceFactoryDep = {
|
||||||
serverCfgDAL: TSuperAdminDALFactory;
|
serverCfgDAL: TSuperAdminDALFactory;
|
||||||
@@ -209,9 +209,25 @@ export const superAdminServiceFactory = ({
|
|||||||
return { token, user: userInfo, organization };
|
return { token, user: userInfo, organization };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => {
|
||||||
|
return userDAL.getUsersByFilter({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
searchTerm,
|
||||||
|
sortBy: "username"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (userId: string) => {
|
||||||
|
const user = await userDAL.deleteById(userId);
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initServerCfg,
|
initServerCfg,
|
||||||
updateServerCfg,
|
updateServerCfg,
|
||||||
adminSignUp
|
adminSignUp,
|
||||||
|
getUsers,
|
||||||
|
deleteUser
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ export type TAdminSignUpDTO = {
|
|||||||
userAgent: string;
|
userAgent: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAdminGetUsersDTO = {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
searchTerm: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum LoginMethod {
|
export enum LoginMethod {
|
||||||
EMAIL = "email",
|
EMAIL = "email",
|
||||||
GOOGLE = "google",
|
GOOGLE = "google",
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import {
|
|||||||
TUserActionsUpdate,
|
TUserActionsUpdate,
|
||||||
TUserEncryptionKeys,
|
TUserEncryptionKeys,
|
||||||
TUserEncryptionKeysInsert,
|
TUserEncryptionKeysInsert,
|
||||||
TUserEncryptionKeysUpdate
|
TUserEncryptionKeysUpdate,
|
||||||
|
TUsers
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TUserDALFactory = ReturnType<typeof userDALFactory>;
|
export type TUserDALFactory = ReturnType<typeof userDALFactory>;
|
||||||
|
|
||||||
@@ -18,6 +19,39 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
const userOrm = ormify(db, TableName.Users);
|
const userOrm = ormify(db, TableName.Users);
|
||||||
const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx);
|
const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx);
|
||||||
|
|
||||||
|
const getUsersByFilter = async ({
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
searchTerm,
|
||||||
|
sortBy
|
||||||
|
}: {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
searchTerm: string;
|
||||||
|
sortBy?: keyof TUsers;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
|
||||||
|
if (searchTerm) {
|
||||||
|
query = query.where((qb) => {
|
||||||
|
void qb
|
||||||
|
.whereILike("email", `%${searchTerm}%`)
|
||||||
|
.orWhereILike("firstName", `%${searchTerm}%`)
|
||||||
|
.orWhereILike("lastName", `%${searchTerm}%`)
|
||||||
|
.orWhereLike("username", `%${searchTerm}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy) {
|
||||||
|
query = query.orderBy(sortBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Users));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Get users by filter" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// USER ENCRYPTION FUNCTIONS
|
// USER ENCRYPTION FUNCTIONS
|
||||||
// -------------------------
|
// -------------------------
|
||||||
const findUserEncKeyByUsername = async ({ username }: { username: string }) => {
|
const findUserEncKeyByUsername = async ({ username }: { username: string }) => {
|
||||||
@@ -159,6 +193,7 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
upsertUserEncryptionKey,
|
upsertUserEncryptionKey,
|
||||||
createUserEncryption,
|
createUserEncryption,
|
||||||
findOneUserAction,
|
findOneUserAction,
|
||||||
createUserAction
|
createUserAction,
|
||||||
|
getUsersByFilter
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -301,7 +301,7 @@ export const userServiceFactory = ({
|
|||||||
toggleUserMfa,
|
toggleUserMfa,
|
||||||
updateUserName,
|
updateUserName,
|
||||||
updateAuthMethods,
|
updateAuthMethods,
|
||||||
deleteMe,
|
deleteUser,
|
||||||
getMe,
|
getMe,
|
||||||
createUserAction,
|
createUserAction,
|
||||||
getUserAction,
|
getUserAction,
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { useCreateAdminUser, useUpdateServerConfig } from "./mutation";
|
export { useAdminDeleteUser, useCreateAdminUser, useUpdateServerConfig } from "./mutation";
|
||||||
export { useGetServerConfig } from "./queries";
|
export { useAdminGetUsers, useGetServerConfig } from "./queries";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
|
|
||||||
import { organizationKeys } from "../organization/queries";
|
import { organizationKeys } from "../organization/queries";
|
||||||
import { User } from "../users/types";
|
import { User } from "../users/types";
|
||||||
import { adminQueryKeys } from "./queries";
|
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
||||||
import { TCreateAdminUserDTO, TServerConfig } from "./types";
|
import { TCreateAdminUserDTO, TServerConfig } from "./types";
|
||||||
|
|
||||||
export const useCreateAdminUser = () => {
|
export const useCreateAdminUser = () => {
|
||||||
@@ -43,3 +43,19 @@ export const useUpdateServerConfig = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAdminDeleteUser = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (userId: string) => {
|
||||||
|
await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}`);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [adminStandaloneKeys.getUsers]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { TServerConfig } from "./types";
|
import { User } from "../types";
|
||||||
|
import { AdminGetUsersFilters, TServerConfig } from "./types";
|
||||||
|
|
||||||
|
export const adminStandaloneKeys = {
|
||||||
|
getUsers: "get-users"
|
||||||
|
};
|
||||||
|
|
||||||
export const adminQueryKeys = {
|
export const adminQueryKeys = {
|
||||||
serverConfig: () => ["server-config"] as const
|
serverConfig: () => ["server-config"] as const,
|
||||||
|
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchServerConfig = async () => {
|
const fetchServerConfig = async () => {
|
||||||
@@ -32,3 +38,24 @@ export const useGetServerConfig = ({
|
|||||||
...options,
|
...options,
|
||||||
enabled: options?.enabled ?? true
|
enabled: options?.enabled ?? true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: adminQueryKeys.getUsers(filters),
|
||||||
|
queryFn: async ({ pageParam }) => {
|
||||||
|
const { data } = await apiRequest.get<{ users: User[] }>(
|
||||||
|
"/api/v1/admin/user-management/users",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
...filters,
|
||||||
|
offset: pageParam
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.users;
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage, pages) =>
|
||||||
|
lastPage.length !== 0 ? pages.length * filters.limit : undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -37,3 +37,8 @@ export type TCreateAdminUserDTO = {
|
|||||||
verifier: string;
|
verifier: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminGetUsersFilters = {
|
||||||
|
limit: number;
|
||||||
|
searchTerm: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export {
|
|||||||
useAddUserToOrg,
|
useAddUserToOrg,
|
||||||
useCreateAPIKey,
|
useCreateAPIKey,
|
||||||
useDeleteAPIKey,
|
useDeleteAPIKey,
|
||||||
|
useDeleteMe,
|
||||||
useDeleteOrgMembership,
|
useDeleteOrgMembership,
|
||||||
useDeleteUser,
|
|
||||||
useGetMyAPIKeys,
|
useGetMyAPIKeys,
|
||||||
useGetMyAPIKeysV2,
|
useGetMyAPIKeysV2,
|
||||||
useGetMyIp,
|
useGetMyIp,
|
||||||
|
|||||||
@@ -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,7 +41,7 @@ export const fetchUserDetails = async () => {
|
|||||||
|
|
||||||
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
|
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
|
||||||
|
|
||||||
export const useDeleteUser = () => {
|
export const useDeleteMe = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
+3
-3
@@ -2,17 +2,17 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
import { useDeleteUser } from "@app/hooks/api";
|
import { useDeleteMe } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
export const DeleteAccountSection = () => {
|
export const DeleteAccountSection = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"deleteAccount"
|
"deleteAccount"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const { mutateAsync: deleteUserMutateAsync, isLoading } = useDeleteUser();
|
const { mutateAsync: deleteUserMutateAsync, isLoading } = useDeleteMe();
|
||||||
|
|
||||||
const handleDeleteAccountSubmit = async () => {
|
const handleDeleteAccountSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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,161 @@
|
|||||||
|
import { 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 {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useUser } from "@app/context";
|
||||||
|
import { useDebounce, usePopUp } from "@app/hooks";
|
||||||
|
import { useAdminDeleteUser, useAdminGetUsers } from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const UserPanelTable = ({
|
||||||
|
handlePopUpOpen
|
||||||
|
}: {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["removeUser"]>,
|
||||||
|
data: {
|
||||||
|
username: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
}) => {
|
||||||
|
const [searchUserFilter, setSearchUserFilter] = useState("");
|
||||||
|
const { user } = useUser();
|
||||||
|
const userId = user?.id || "";
|
||||||
|
const debounedSearchTerm = useDebounce(searchUserFilter, 500);
|
||||||
|
|
||||||
|
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
|
||||||
|
limit: 20,
|
||||||
|
searchTerm: debounedSearchTerm
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEmpty = !isLoading && !data?.pages?.[0].length;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
value={searchUserFilter}
|
||||||
|
onChange={(e) => setSearchUserFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search users..."
|
||||||
|
/>
|
||||||
|
<div className="mt-4">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-5/12">Name</Th>
|
||||||
|
<Th className="w-5/12">Username</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} innerKey="users" />}
|
||||||
|
{!isLoading &&
|
||||||
|
data?.pages?.map((users) =>
|
||||||
|
users.map(({ username, email, firstName, lastName, id }) => {
|
||||||
|
const name = firstName || lastName ? `${firstName} ${lastName}` : "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`user-${id}`} className="w-full">
|
||||||
|
<Td className="w-5/12">{name}</Td>
|
||||||
|
<Td className="w-5/12">{email}</Td>
|
||||||
|
<Td>
|
||||||
|
{userId !== id && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<IconButton
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
isDisabled={userId === id}
|
||||||
|
onClick={() => handlePopUpOpen("removeUser", { username, id })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
|
||||||
|
</TableContainer>
|
||||||
|
{!isEmpty && (
|
||||||
|
<Button
|
||||||
|
className="mt-4 py-3 text-sm"
|
||||||
|
isFullWidth
|
||||||
|
variant="star"
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
|
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
>
|
||||||
|
{hasNextPage ? "Load More" : "End of list"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserPanel = () => {
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"removeUser"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteUser } = useAdminDeleteUser();
|
||||||
|
|
||||||
|
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>
|
||||||
|
<UserPanelTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
<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