Finish preliminary v2 audit logs

This commit is contained in:
Tuan Dang
2023-08-05 16:55:06 +07:00
parent 6fbcbc4807
commit 142fcf0a01
100 changed files with 1885 additions and 1269 deletions
@@ -0,0 +1,17 @@
import {
LogsSection
} from "./components";
export const LogsPage = () => {
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">
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
<div />
</div>
<LogsSection />
</div>
</div>
);
}
@@ -0,0 +1,147 @@
import { Control, Controller, UseFormReset } from "react-hook-form";
import {
FormControl,
Select,
SelectItem,
Button
} from "@app/components/v2";
import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants";
import { useWorkspace } from "@app/context";
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
import { Actor } from "~/hooks/api/auditLogs/types";
import { ActorType } from "~/hooks/api/auditLogs/enums";
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AuditLogFilterFormData } from "./LogsSection";
const eventTypes = Object.entries(eventToNameMap).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) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? "");
const renderActorSelectItem = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<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}`}>
{actor.metadata.name}
</SelectItem>
);
}
}
return (
<div className="flex justify-between items-center">
<div className="flex items-center">
<div className="w-40 mr-4">
<Controller
control={control}
name="eventType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Event"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{eventTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
{!isLoading && data && data.length > 0 && (
<div className="w-40 mr-4">
<Controller
control={control}
name="actor"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Actor"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{data.map((actor) => renderActorSelectItem(actor))}
</Select>
</FormControl>
)}
/>
</div>
)}
<div className="w-40">
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Source"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{userAgentTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
</div>
<div>
<Button
isLoading={false}
colorSchema="primary"
variant="outline_bg"
type="submit"
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-2" />}
onClick={() => reset({
eventType: "",
actor: "",
userAgentType: ""
})}
>
Clear filters
</Button>
</div>
</div>
);
}
@@ -0,0 +1,49 @@
import { useForm } from "react-hook-form";
import { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums";
const schema = yup.object({
eventType: yup.string()
.oneOf(Object.values(EventType), 'Invalid event type'),
actor: yup.string(),
userAgentType: yup.string()
.oneOf(Object.values(UserAgentType), 'Invalid user agent type'),
}).required();
export type AuditLogFilterFormData = yup.InferType<typeof schema>;
export const LogsSection = () => {
const {
control,
reset,
watch,
} = useForm<AuditLogFilterFormData>({
resolver: yupResolver(schema)
});
const eventType = watch("eventType") as EventType | undefined;
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor") as string | undefined;
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}
/>
<LogsTable
eventType={eventType}
userAgentType={userAgentType}
actor={actor}
/>
</div>
);
}
@@ -0,0 +1,70 @@
import { faFile } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetAuditLogs } from "@app/hooks/api";
import { LogsTableRow } from "./LogsTableRow";
import { EventType, UserAgentType } from "~/hooks/api/auditLogs/enums";
type Props = {
eventType: EventType | undefined;
userAgentType: UserAgentType | undefined;
actor: string | undefined;
}
export const LogsTable = ({
eventType,
userAgentType,
actor
}: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", {
eventType,
userAgentType,
actor
});
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Timestamp</Th>
<Th>Event</Th>
<Th>Actor</Th>
<Th>Source</Th>
<Th>Metadata</Th>
</Tr>
</THead>
<TBody>
{!isLoading && data && data.map((auditLog) => (
<LogsTableRow
auditLog={auditLog}
key={`audit-log-${auditLog._id}`}
/>
))}
{isLoading && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
{!isLoading && data && data.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No audit logs on file"
icon={faFile}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1,114 @@
import { AuditLog, Actor, Event } from "~/hooks/api/auditLogs/types";
import { ActorType, EventType } from "~/hooks/api/auditLogs/enums";
import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants";
import {
Td,
Tr
} from "@app/components/v2";
type Props = {
auditLog: AuditLog
}
export const LogsTableRow = ({
auditLog
}: Props) => {
const renderActor = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<Td>
<p>{actor.metadata.email}</p>
<p>User</p>
</Td>
);
case ActorType.SERVICE:
return (
<Td>
<p>{`${actor.metadata.name}`}</p>
<p>Service token</p>
</Td>
);
}
}
const renderMetadata = (event: Event) => {
switch (event.type) {
case EventType.GET_SECRETS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`# Secrets: ${event.metadata.numberOfSecrets}`}</p>
</Td>
);
case EventType.GET_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.CREATE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.UPDATE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.DELETE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
default:
return (
<Td>Test</Td>
);
}
}
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
let hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, '0');
// convert from 24h to 12h format
const period = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`;
return formattedDate;
}
return (
<Tr className={`log-${auditLog._id} h-10`}>
<Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{renderActor(auditLog.actor)}
<Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
<p>{auditLog.ipAddress}</p>
</Td>
{renderMetadata(auditLog.event)}
</Tr>
);
}
@@ -0,0 +1 @@
export { LogsSection } from "./LogsSection";
@@ -0,0 +1 @@
export { LogsPage } from "./LogsPage";