feat(audit-log): added index over workspace key and changed pagination to load more type

This commit is contained in:
Akhil Mohan
2023-11-21 14:36:06 +05:30
parent d909ff6a97
commit 9d9e830d73
9 changed files with 753 additions and 815 deletions
@@ -611,14 +611,9 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
}
: {})
};
const auditLogs = await AuditLog.find(query).sort({ createdAt: -1 }).skip(offset).limit(limit);
const totalCount = await AuditLog.countDocuments(query);
return res.status(200).send({
auditLogs,
totalCount
auditLogs
});
};
@@ -679,11 +674,7 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
}
}));
const actors = [
...userActors,
...serviceActors,
...serviceV3Actors
];
const actors = [...userActors, ...serviceActors, ...serviceV3Actors];
return res.status(200).send({
actors
+4 -10
View File
@@ -1,13 +1,6 @@
import { Schema, Types, model } from "mongoose";
import {
ActorType,
EventType,
UserAgentType
} from "./enums";
import {
Actor,
Event
} from "./types";
import { ActorType, EventType, UserAgentType } from "./enums";
import { Actor, Event } from "./types";
export interface IAuditLog {
actor: Actor;
@@ -38,7 +31,8 @@ const auditLogSchema = new Schema<IAuditLog>(
},
workspace: {
type: Schema.Types.ObjectId,
required: false
required: false,
index: true
},
ipAddress: {
type: String,
+2 -2
View File
@@ -121,8 +121,8 @@ export const GetWorkspaceAuditLogsV1 = z.object({
userAgentType: z.nativeEnum(UserAgentType).nullable().optional(),
startDate: z.string().datetime().nullable().optional(),
endDate: z.string().datetime().nullable().optional(),
offset: z.coerce.number(),
limit: z.coerce.number(),
offset: z.coerce.number().default(0),
limit: z.coerce.number().default(20),
actor: z.string().nullish().optional()
})
});
+27 -40
View File
@@ -1,59 +1,46 @@
import { useQuery } from "@tanstack/react-query";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
Actor,
AuditLog,
AuditLogFilters
} from "./types";
import { Actor, AuditLog, AuditLogFilters } from "./types";
export const workspaceKeys = {
getAuditLogs: (workspaceId: string, filters: AuditLogFilters) => [{ workspaceId, filters }, "audit-logs"] as const,
getAuditLogActorFilterOpts: (workspaceId: string) => [{ workspaceId }, "audit-log-actor-filters"] as const
}
getAuditLogs: (workspaceId: string, filters: AuditLogFilters) =>
[{ workspaceId, filters }, "audit-logs"] as const,
getAuditLogActorFilterOpts: (workspaceId: string) =>
[{ workspaceId }, "audit-log-actor-filters"] as const
};
export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => {
return useQuery({
return useInfiniteQuery({
queryKey: workspaceKeys.getAuditLogs(workspaceId, filters),
queryFn: async () => {
const params = new URLSearchParams();
if (filters.eventType) {
params.append("eventType", filters.eventType);
queryFn: async ({ pageParam }) => {
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
`/api/v1/workspace/${workspaceId}/audit-logs`,
{
params: {
...filters,
offset: pageParam,
startDate: filters?.startDate?.toISOString(),
endDate: filters?.endDate?.toISOString()
}
if (filters.userAgentType) {
params.append("userAgentType", filters.userAgentType);
}
if (filters.actor) {
params.append("actor", filters.actor);
}
if (filters.startDate) {
params.append("startDate", filters.startDate.toISOString());
}
if (filters.endDate) {
params.append("endDate", filters.endDate.toISOString());
}
params.append("offset", String(filters.offset));
params.append("limit", String(filters.limit));
const { data } = await apiRequest.get<{ auditLogs: AuditLog[], totalCount: number }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params });
return data;
}
);
return data.auditLogs;
},
getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined
});
}
};
export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getAuditLogActorFilterOpts(workspaceId),
queryFn: async () => {
const { data } = await apiRequest.get<{ actors: Actor[] }>(`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`);
const { data } = await apiRequest.get<{ actors: Actor[] }>(
`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`
);
return data.actors;
}
});
}
};
+50 -59
View File
@@ -1,8 +1,4 @@
import {
ActorType,
EventType,
UserAgentType
} from "./enums";
import { ActorType, EventType, UserAgentType } from "./enums";
enum Permission {
READ = "read",
@@ -40,10 +36,7 @@ export interface ServiceActorV3 {
metadata: ServiceActorMetadata;
}
export type Actor =
| UserActor
| ServiceActor
| ServiceActorV3;
export type Actor = UserActor | ServiceActor | ServiceActorV3;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
@@ -73,7 +66,7 @@ interface CreateSecretEvent {
secretId: string;
secretKey: string;
secretVersion: number;
}
};
}
interface UpdateSecretEvent {
@@ -84,7 +77,7 @@ interface UpdateSecretEvent {
secretId: string;
secretKey: string;
secretVersion: number;
}
};
}
interface DeleteSecretEvent {
@@ -95,28 +88,28 @@ interface DeleteSecretEvent {
secretId: string;
secretKey: string;
secretVersion: number;
}
};
}
interface GetWorkspaceKeyEvent {
type: EventType.GET_WORKSPACE_KEY,
type: EventType.GET_WORKSPACE_KEY;
metadata: {
keyId: string;
}
};
}
interface AuthorizeIntegrationEvent {
type: EventType.AUTHORIZE_INTEGRATION;
metadata: {
integration: string;
}
};
}
interface UnauthorizeIntegrationEvent {
type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: {
integration: string;
}
};
}
interface CreateIntegrationEvent {
@@ -135,7 +128,7 @@ interface CreateIntegrationEvent {
targetServiceId?: string;
path?: string;
region?: string;
}
};
}
interface DeleteIntegrationEvent {
@@ -154,7 +147,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string;
path?: string;
region?: string;
}
};
}
interface AddTrustedIPEvent {
@@ -163,7 +156,7 @@ interface AddTrustedIPEvent {
trustedIpId: string;
ipAddress: string;
prefix?: number;
}
};
}
interface UpdateTrustedIPEvent {
@@ -172,7 +165,7 @@ interface UpdateTrustedIPEvent {
trustedIpId: string;
ipAddress: string;
prefix?: number;
}
};
}
interface DeleteTrustedIPEvent {
@@ -181,7 +174,7 @@ interface DeleteTrustedIPEvent {
trustedIpId: string;
ipAddress: string;
prefix?: number;
}
};
}
interface CreateServiceTokenEvent {
@@ -192,7 +185,7 @@ interface CreateServiceTokenEvent {
environment: string;
secretPath: string;
}>;
}
};
}
interface DeleteServiceTokenEvent {
@@ -203,7 +196,7 @@ interface DeleteServiceTokenEvent {
environment: string;
secretPath: string;
}>;
}
};
}
interface CreateServiceTokenV3Event {
@@ -213,7 +206,7 @@ interface CreateServiceTokenV3Event {
isActive: boolean;
scopes: Array<Scope>;
expiresAt?: Date;
}
};
}
interface UpdateServiceTokenV3Event {
@@ -223,7 +216,7 @@ interface UpdateServiceTokenV3Event {
isActive?: boolean;
scopes?: Array<Scope>;
expiresAt?: Date;
}
};
}
interface DeleteServiceTokenV3Event {
@@ -233,7 +226,7 @@ interface DeleteServiceTokenV3Event {
isActive: boolean;
scopes: Array<Scope>;
expiresAt?: Date;
}
};
}
interface CreateEnvironmentEvent {
@@ -241,7 +234,7 @@ interface CreateEnvironmentEvent {
metadata: {
name: string;
slug: string;
}
};
}
interface UpdateEnvironmentEvent {
@@ -251,7 +244,7 @@ interface UpdateEnvironmentEvent {
newName: string;
oldSlug: string;
newSlug: string;
}
};
}
interface DeleteEnvironmentEvent {
@@ -259,7 +252,7 @@ interface DeleteEnvironmentEvent {
metadata: {
name: string;
slug: string;
}
};
}
interface AddWorkspaceMemberEvent {
@@ -267,7 +260,7 @@ interface AddWorkspaceMemberEvent {
metadata: {
userId: string;
email: string;
}
};
}
interface RemoveWorkspaceMemberEvent {
@@ -275,7 +268,7 @@ interface RemoveWorkspaceMemberEvent {
metadata: {
userId: string;
email: string;
}
};
}
interface CreateFolderEvent {
@@ -285,7 +278,7 @@ interface CreateFolderEvent {
folderId: string;
folderName: string;
folderPath: string;
}
};
}
interface UpdateFolderEvent {
@@ -296,7 +289,7 @@ interface UpdateFolderEvent {
oldFolderName: string;
newFolderName: string;
folderPath: string;
}
};
}
interface DeleteFolderEvent {
@@ -306,54 +299,54 @@ interface DeleteFolderEvent {
folderId: string;
folderName: string;
folderPath: string;
}
};
}
interface CreateWebhookEvent {
type: EventType.CREATE_WEBHOOK,
type: EventType.CREATE_WEBHOOK;
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
}
};
}
interface UpdateWebhookStatusEvent {
type: EventType.UPDATE_WEBHOOK_STATUS,
type: EventType.UPDATE_WEBHOOK_STATUS;
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
}
};
}
interface DeleteWebhookEvent {
type: EventType.DELETE_WEBHOOK,
type: EventType.DELETE_WEBHOOK;
metadata: {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
}
};
}
interface GetSecretImportsEvent {
type: EventType.GET_SECRET_IMPORTS,
type: EventType.GET_SECRET_IMPORTS;
metadata: {
environment: string;
secretImportId: string;
folderId: string;
numberOfImports: number;
}
};
}
interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT,
type: EventType.CREATE_SECRET_IMPORT;
metadata: {
secretImportId: string;
folderId: string;
@@ -361,11 +354,11 @@ interface CreateSecretImportEvent {
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
}
};
}
interface UpdateSecretImportEvent {
type: EventType.UPDATE_SECRET_IMPORT,
type: EventType.UPDATE_SECRET_IMPORT;
metadata: {
secretImportId: string;
folderId: string;
@@ -374,16 +367,16 @@ interface UpdateSecretImportEvent {
orderBefore: {
environment: string;
secretPath: string;
}[],
}[];
orderAfter: {
environment: string;
secretPath: string;
}[]
}
}[];
};
}
interface DeleteSecretImportEvent {
type: EventType.DELETE_SECRET_IMPORT,
type: EventType.DELETE_SECRET_IMPORT;
metadata: {
secretImportId: string;
folderId: string;
@@ -391,31 +384,30 @@ interface DeleteSecretImportEvent {
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
}
};
}
interface UpdateUserRole {
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
metadata: {
userId: string;
email: string;
oldRole: string;
newRole: string;
}
};
}
interface UpdateUserDeniedPermissions {
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
metadata: {
userId: string;
email: string;
deniedPermissions: {
environmentSlug: string;
ability: string;
}[]
}[];
};
}
}
export type Event =
| GetSecretsEvent
@@ -465,14 +457,13 @@ export type AuditLog = {
userAgentType: UserAgentType;
createdAt: string;
updatedAt: string;
}
};
export type AuditLogFilters = {
eventType?: EventType;
userAgentType?: UserAgentType;
actor?: string;
offset: number;
limit: number;
startDate?: Date;
endDate?: Date;
}
};
@@ -8,7 +8,7 @@ export const AuditLogsPage = withProjectPermission(
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
<div className="my-6">
<div className="py-6 sticky top-0 z-10 bg-bunker-800">
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
<div />
</div>
@@ -3,13 +3,7 @@ import { Control, Controller, UseFormReset } from "react-hook-form";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
DatePicker,
FormControl,
Select,
SelectItem
} from "@app/components/v2";
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
@@ -19,17 +13,17 @@ import { Actor } from "@app/hooks/api/auditLogs/types";
import { AuditLogFilterFormData } from "./types";
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({ label, value }));
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
label,
value
}));
type Props = {
control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>;
}
};
export const LogsFilter = ({
control,
reset
}: Props) => {
export const LogsFilter = ({ control, reset }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
@@ -40,19 +34,28 @@ export const LogsFilter = ({
switch (actor.type) {
case ActorType.USER:
return (
<SelectItem value={`${actor.type}-${actor.metadata.userId}`} key={`user-actor-filter-${actor.metadata.userId}`}>
<SelectItem
value={`${actor.type}-${actor.metadata.userId}`}
key={`user-actor-filter-${actor.metadata.userId}`}
>
{actor.metadata.email}
</SelectItem>
);
case ActorType.SERVICE:
return (
<SelectItem value={`${actor.type}-${actor.metadata.serviceId}`} key={`service-actor-filter-${actor.metadata.serviceId}`}>
<SelectItem
value={`${actor.type}-${actor.metadata.serviceId}`}
key={`service-actor-filter-${actor.metadata.serviceId}`}
>
{actor.metadata.name}
</SelectItem>
);
case ActorType.SERVICE_V3:
return (
<SelectItem value={`${actor.type}-${actor.metadata.serviceId}`} key={`service-actor-v3-filter-${actor.metadata.serviceId}`}>
<SelectItem
value={`${actor.type}-${actor.metadata.serviceId}`}
key={`service-actor-v3-filter-${actor.metadata.serviceId}`}
>
{actor.metadata.name}
</SelectItem>
);
@@ -63,11 +66,10 @@ export const LogsFilter = ({
</SelectItem>
);
}
}
};
return (
<div className="flex justify-between items-center">
<div className="flex justify-between items-center sticky top-20 z-10 bg-bunker-800">
<div className="flex items-center">
<Controller
control={control}
@@ -155,7 +157,7 @@ export const LogsFilter = ({
>
<DatePicker
value={field.value || undefined}
onChange={date => {
onChange={(date) => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
@@ -174,14 +176,10 @@ export const LogsFilter = ({
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="End date"
errorText={error?.message}
isError={Boolean(error)}
>
<FormControl label="End date" errorText={error?.message} isError={Boolean(error)}>
<DatePicker
value={field.value || undefined}
onChange={date => {
onChange={(date) => {
onChange(date);
setIsEndDatePickerOpen(false);
}}
@@ -203,17 +201,19 @@ export const LogsFilter = ({
variant="outline_bg"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
onClick={() => reset({
onClick={() =>
reset({
eventType: undefined,
actor: undefined,
userAgentType: undefined,
startDate: undefined,
endDate: undefined
})}
})
}
>
Clear filters
</Button>
</div>
</div>
);
}
};
@@ -16,16 +16,9 @@ export const LogsSection = () => {
const { subscription } = useSubscription();
const router = useRouter();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan"
] as const);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const {
control,
reset,
watch,
setValue,
} = useForm<AuditLogFilterFormData>({
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
resolver: yupResolver(auditLogFilterFormSchema),
defaultValues: {
page: 1,
@@ -46,45 +39,28 @@ export const LogsSection = () => {
const startDate = watch("startDate");
const endDate = watch("endDate");
const page = watch("page") as number;
const perPage = watch("perPage") as number;
return (
<div
// className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
{/* <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Audit Logs
</h2>
</div> */}
<LogsFilter
control={control}
reset={reset}
/>
<div>
<LogsFilter control={control} reset={reset} />
<LogsTable
eventType={eventType}
userAgentType={userAgentType}
actor={actor}
startDate={startDate}
endDate={endDate}
page={page}
perPage={perPage}
setValue={setValue}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => {
if (!isOpen) {
router.back();
return;
}
handlePopUpToggle("upgradePlan", isOpen)
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use audit logs if you switch to a paid Infisical plan."
/>
</div>
);
}
};
@@ -1,8 +1,9 @@
import { Fragment } from "react";
import { faFile } from "@fortawesome/free-solid-svg-icons";
import {
Button,
EmptyState,
Pagination,
Table,
TableContainer,
TableSkeleton,
@@ -10,13 +11,13 @@ import {
Td,
Th,
THead,
Tr} from "@app/components/v2";
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetAuditLogs } from "@app/hooks/api";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { LogsTableRow } from "./LogsTableRow";
import { SetValueType } from "./types";
type Props = {
eventType?: EventType;
@@ -24,33 +25,28 @@ type Props = {
actor?: string;
startDate?: Date;
endDate?: Date;
page: number;
perPage: number;
setValue: SetValueType;
}
};
export const LogsTable = ({
eventType,
userAgentType,
actor,
startDate,
endDate,
page,
perPage,
setValue
}: Props) => {
const AUDIT_LOG_LIMIT = 15;
export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", {
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
currentWorkspace?._id ?? "",
{
eventType,
userAgentType,
actor,
startDate,
endDate,
offset: (page - 1) * perPage,
limit: perPage
});
limit: AUDIT_LOG_LIMIT
}
);
const isEmpty = !isLoading && !data?.pages?.[0].length;
return (
<div>
<TableContainer>
<Table>
<THead>
@@ -63,34 +59,37 @@ export const LogsTable = ({
</Tr>
</THead>
<TBody>
{!isLoading && data?.auditLogs && data?.auditLogs.map((auditLog) => (
<LogsTableRow
auditLog={auditLog}
key={`audit-log-${auditLog._id}`}
/>
{!isLoading &&
data?.pages?.map((group, i) => (
<Fragment key={`auditlog-item-${i + 1}`}>
{group.map((auditLog) => (
<LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog._id}`} />
))}
</Fragment>
))}
{isLoading && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
{!isLoading && data?.auditLogs && data?.auditLogs.length === 0 && (
{isEmpty && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No audit logs on file"
icon={faFile}
/>
<EmptyState title="No audit logs on file" icon={faFile} />
</Td>
</Tr>
)}
</TBody>
</Table>
{!isLoading && data?.totalCount !== undefined && (
<Pagination
count={data?.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setValue("page", newPage)}
onChangePerPage={(newPerPage) => setValue("perPage", newPerPage)}
/>
)}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 mb-20 py-3 px-4 text-sm"
isFullWidth
variant="star"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of logs"}
</Button>
)}
</div>
);
}
};