mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
misc: added pagination and moved to admin route
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().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({
|
||||
method: "POST",
|
||||
url: "/signup",
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
|
||||
@@ -92,75 +91,6 @@ 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",
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AuthMethod } from "../auth/auth-type";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||
import { LoginMethod, TAdminSignUpDTO } from "./super-admin-types";
|
||||
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
|
||||
|
||||
type TSuperAdminServiceFactoryDep = {
|
||||
serverCfgDAL: TSuperAdminDALFactory;
|
||||
@@ -209,9 +209,24 @@ export const superAdminServiceFactory = ({
|
||||
return { token, user: userInfo, organization };
|
||||
};
|
||||
|
||||
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => {
|
||||
return userDAL.getUsersByFilter({
|
||||
limit,
|
||||
offset,
|
||||
searchTerm
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUser = async (userId: string) => {
|
||||
const user = await userDAL.deleteById(userId);
|
||||
return user;
|
||||
};
|
||||
|
||||
return {
|
||||
initServerCfg,
|
||||
updateServerCfg,
|
||||
adminSignUp
|
||||
adminSignUp,
|
||||
getUsers,
|
||||
deleteUser
|
||||
};
|
||||
};
|
||||
|
||||
@@ -16,6 +16,12 @@ export type TAdminSignUpDTO = {
|
||||
userAgent: string;
|
||||
};
|
||||
|
||||
export type TAdminGetUsersDTO = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export enum LoginMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TUserEncryptionKeysUpdate
|
||||
} from "@app/db/schemas";
|
||||
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>;
|
||||
|
||||
@@ -18,6 +18,32 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
const userOrm = ormify(db, TableName.Users);
|
||||
const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx);
|
||||
|
||||
const getUsersByFilter = async ({
|
||||
limit,
|
||||
offset,
|
||||
searchTerm
|
||||
}: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
searchTerm: string;
|
||||
}) => {
|
||||
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}%`);
|
||||
});
|
||||
}
|
||||
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
|
||||
// -------------------------
|
||||
const findUserEncKeyByUsername = async ({ username }: { username: string }) => {
|
||||
@@ -159,6 +185,7 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
upsertUserEncryptionKey,
|
||||
createUserEncryption,
|
||||
findOneUserAction,
|
||||
createUserAction
|
||||
createUserAction,
|
||||
getUsersByFilter
|
||||
};
|
||||
};
|
||||
|
||||
@@ -295,10 +295,6 @@ export const userServiceFactory = ({
|
||||
return updatedOrgMembership.projectFavorites;
|
||||
};
|
||||
|
||||
const listUsers = async () => {
|
||||
return userDAL.find({});
|
||||
};
|
||||
|
||||
return {
|
||||
sendEmailVerificationCode,
|
||||
verifyEmailVerificationCode,
|
||||
@@ -312,7 +308,6 @@ export const userServiceFactory = ({
|
||||
unlockUser,
|
||||
getUserPrivateKey,
|
||||
getUserProjectFavorites,
|
||||
updateUserProjectFavorites,
|
||||
listUsers
|
||||
updateUserProjectFavorites
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { useCreateAdminUser, useUpdateServerConfig } from "./mutation";
|
||||
export { useGetServerConfig } from "./queries";
|
||||
export { useCreateAdminUser, useDeleteUser, useUpdateServerConfig } from "./mutation";
|
||||
export { useAdminGetUsers, useGetServerConfig } from "./queries";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { organizationKeys } from "../organization/queries";
|
||||
import { User } from "../users/types";
|
||||
import { adminQueryKeys } from "./queries";
|
||||
import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
||||
import { TCreateAdminUserDTO, TServerConfig } from "./types";
|
||||
|
||||
export const useCreateAdminUser = () => {
|
||||
@@ -43,3 +43,19 @@ export const useUpdateServerConfig = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteUser = () => {
|
||||
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 { TServerConfig } from "./types";
|
||||
import { User } from "../types";
|
||||
import { AdminGetUsersFilters, TServerConfig } from "./types";
|
||||
|
||||
export const adminStandaloneKeys = {
|
||||
getUsers: "get-users"
|
||||
};
|
||||
|
||||
export const adminQueryKeys = {
|
||||
serverConfig: () => ["server-config"] as const
|
||||
serverConfig: () => ["server-config"] as const,
|
||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const
|
||||
};
|
||||
|
||||
const fetchServerConfig = async () => {
|
||||
@@ -32,3 +38,24 @@ export const useGetServerConfig = ({
|
||||
...options,
|
||||
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;
|
||||
salt: string;
|
||||
};
|
||||
|
||||
export type AdminGetUsersFilters = {
|
||||
limit: number;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export {
|
||||
useAddUserToWsE2EE,
|
||||
useAddUserToWsNonE2EE,
|
||||
useDeleteUser,
|
||||
useSendEmailVerificationCode,
|
||||
useVerifyEmailVerificationCode
|
||||
} from "./mutation";
|
||||
@@ -20,7 +19,6 @@ export {
|
||||
useGetOrgUsers,
|
||||
useGetUser,
|
||||
useGetUserAction,
|
||||
useListUsers,
|
||||
useLogoutUser,
|
||||
useRegisterUserAction,
|
||||
useRevokeMySessions,
|
||||
|
||||
@@ -112,17 +112,3 @@ 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -41,14 +41,6 @@ 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 useDeleteMe = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMemo, useState } from "react";
|
||||
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,
|
||||
@@ -18,31 +19,103 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteUser, useListUsers } from "@app/hooks/api";
|
||||
import { useDebounce, usePopUp } from "@app/hooks";
|
||||
import { useAdminGetUsers, useDeleteUser } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const UserPanelTable = ({
|
||||
searchUserFilter,
|
||||
handlePopUpOpen
|
||||
}: {
|
||||
searchUserFilter: string;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeUser"]>,
|
||||
data: {
|
||||
username: string;
|
||||
id: string;
|
||||
}
|
||||
) => void;
|
||||
}) => {
|
||||
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 (
|
||||
<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 [searchUserFilter, setSearchUserFilter] = 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 [searchUserFilter, setSearchUserFilter] = useState("");
|
||||
|
||||
const filterdUsers = useMemo(
|
||||
() =>
|
||||
users?.filter(
|
||||
({ firstName, lastName, username, email }) =>
|
||||
firstName?.toLowerCase().includes(searchUserFilter.toLowerCase()) ||
|
||||
lastName?.toLowerCase().includes(searchUserFilter.toLowerCase()) ||
|
||||
username?.toLowerCase().includes(searchUserFilter.toLowerCase()) ||
|
||||
email?.toLowerCase().includes(searchUserFilter.toLowerCase())
|
||||
),
|
||||
[users, searchUserFilter]
|
||||
);
|
||||
const { mutateAsync: deleteUser } = useDeleteUser();
|
||||
|
||||
const handleRemoveUser = async () => {
|
||||
const { id } = popUp?.removeUser?.data as { id: string; username: string };
|
||||
@@ -74,54 +147,7 @@ export const UserPanel = () => {
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search users..."
|
||||
/>
|
||||
|
||||
<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>
|
||||
<UserPanelTable searchUserFilter={searchUserFilter} handlePopUpOpen={handlePopUpOpen} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeUser.isOpen}
|
||||
deleteKey="remove"
|
||||
|
||||
Reference in New Issue
Block a user