mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-22 12:10:35 +02:00
✨ Implement IP filtering for admin access logs
This commit is contained in:
@@ -88,7 +88,27 @@ export const queryAccessAuditLogsQuery = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.default("0")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.int().nonnegative())
|
.pipe(z.int().nonnegative()),
|
||||||
|
ip: z
|
||||||
|
.preprocess((val) => {
|
||||||
|
if (val === undefined || val === null || val === "") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
// the array is returned as this
|
||||||
|
if (typeof val === "string") {
|
||||||
|
return val.split(",");
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, z.array(z.string()))
|
||||||
|
.optional()
|
||||||
|
.catch([])
|
||||||
|
.openapi({
|
||||||
|
type: "array",
|
||||||
|
description: "Filter by IP adresses"
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryAccessAuditLogsParams = z.object({
|
export const queryAccessAuditLogsParams = z.object({
|
||||||
@@ -134,7 +154,8 @@ function getWhere(data: Q) {
|
|||||||
data.type ? eq(accessAuditLog.type, data.type) : undefined,
|
data.type ? eq(accessAuditLog.type, data.type) : undefined,
|
||||||
data.action !== undefined
|
data.action !== undefined
|
||||||
? eq(accessAuditLog.action, data.action)
|
? eq(accessAuditLog.action, data.action)
|
||||||
: undefined
|
: undefined,
|
||||||
|
data.ip ? inArray(accessAuditLog.ip, data.ip) : undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { DateTimeValue } from "@app/components/DateTimePicker";
|
|||||||
import { ArrowUpRight, Key, User } from "lucide-react";
|
import { ArrowUpRight, Key, User } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
|
import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
|
||||||
|
import { ColumnMultiFilterButton } from "@app/components/ColumnMultiFilterButton";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||||
@@ -42,12 +43,14 @@ export default function GeneralPage() {
|
|||||||
resourceId?: string;
|
resourceId?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
actor?: string;
|
actor?: string;
|
||||||
|
ip?: string[];
|
||||||
}>({
|
}>({
|
||||||
action: searchParams.get("action") || undefined,
|
action: searchParams.get("action") || undefined,
|
||||||
type: searchParams.get("type") || undefined,
|
type: searchParams.get("type") || undefined,
|
||||||
resourceId: searchParams.get("resourceId") || undefined,
|
resourceId: searchParams.get("resourceId") || undefined,
|
||||||
location: searchParams.get("location") || undefined,
|
location: searchParams.get("location") || undefined,
|
||||||
actor: searchParams.get("actor") || undefined
|
actor: searchParams.get("actor") || undefined,
|
||||||
|
ip: searchParams.getAll("ip") || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||||
@@ -156,7 +159,7 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
const handleFilterChange = (
|
const handleFilterChange = (
|
||||||
filterType: keyof typeof filters,
|
filterType: keyof typeof filters,
|
||||||
value: string | undefined
|
value: string | string[] | undefined
|
||||||
) => {
|
) => {
|
||||||
const newFilters = { ...filters, [filterType]: value };
|
const newFilters = { ...filters, [filterType]: value };
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
@@ -174,10 +177,13 @@ export default function GeneralPage() {
|
|||||||
) => {
|
) => {
|
||||||
const params = new URLSearchParams(searchParams);
|
const params = new URLSearchParams(searchParams);
|
||||||
Object.entries(newFilters).forEach(([key, value]) => {
|
Object.entries(newFilters).forEach(([key, value]) => {
|
||||||
if (value) {
|
params.delete(key);
|
||||||
|
if (typeof value === "string") {
|
||||||
params.set(key, value);
|
params.set(key, value);
|
||||||
} else {
|
} else if (typeof value !== "undefined" && "length" in value) {
|
||||||
params.delete(key);
|
for (const element of value) {
|
||||||
|
params.append(key, element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
router.replace(`?${params.toString()}`, { scroll: false });
|
||||||
@@ -185,6 +191,7 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
const exportData = async () => {
|
const exportData = async () => {
|
||||||
try {
|
try {
|
||||||
|
const { ip, ...restFilters } = filters;
|
||||||
const params: any = {
|
const params: any = {
|
||||||
timeStart: dateRange.startDate?.date
|
timeStart: dateRange.startDate?.date
|
||||||
? new Date(dateRange.startDate.date).toISOString()
|
? new Date(dateRange.startDate.date).toISOString()
|
||||||
@@ -192,13 +199,20 @@ export default function GeneralPage() {
|
|||||||
timeEnd: dateRange.endDate?.date
|
timeEnd: dateRange.endDate?.date
|
||||||
? new Date(dateRange.endDate.date).toISOString()
|
? new Date(dateRange.endDate.date).toISOString()
|
||||||
: undefined,
|
: undefined,
|
||||||
...filters
|
...restFilters
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await api.get(`/org/${orgId}/logs/access/export`, {
|
// axios serializes arrays as `ip[]=…`, which express's query
|
||||||
responseType: "blob",
|
// parser does not read back as `ip`, so pass them in the URL
|
||||||
params
|
const sp = new URLSearchParams((ip ?? []).map((ip) => ["ip", ip]));
|
||||||
});
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/org/${orgId}/logs/access/export?${sp.toString()}`,
|
||||||
|
{
|
||||||
|
responseType: "blob",
|
||||||
|
params
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
@@ -277,7 +291,24 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "ip",
|
||||||
header: () => <span className="px-2">{t("ip")}</span>
|
header: () => (
|
||||||
|
<span className="px-2">
|
||||||
|
<ColumnMultiFilterButton
|
||||||
|
options={(filters.ip ?? []).map((ip) => ({
|
||||||
|
label: ip,
|
||||||
|
value: ip
|
||||||
|
}))}
|
||||||
|
label={t("ip")}
|
||||||
|
allowArbitraryValues
|
||||||
|
searchPlaceholder={t("ipFilterSearchPlaceholder")}
|
||||||
|
emptyMessage={t("ipFilterEmptyMessage")}
|
||||||
|
selectedValues={filters.ip ?? []}
|
||||||
|
onSelectedValuesChange={(value) =>
|
||||||
|
handleFilterChange("ip", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "location",
|
accessorKey: "location",
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export default function GeneralPage() {
|
|||||||
const exportData = async () => {
|
const exportData = async () => {
|
||||||
try {
|
try {
|
||||||
// Prepare query params for export
|
// Prepare query params for export
|
||||||
|
const { ip, ...restFilters } = filters;
|
||||||
const params: any = {
|
const params: any = {
|
||||||
timeStart: dateRange.startDate?.date
|
timeStart: dateRange.startDate?.date
|
||||||
? new Date(dateRange.startDate.date).toISOString()
|
? new Date(dateRange.startDate.date).toISOString()
|
||||||
@@ -202,11 +203,15 @@ export default function GeneralPage() {
|
|||||||
timeEnd: dateRange.endDate?.date
|
timeEnd: dateRange.endDate?.date
|
||||||
? new Date(dateRange.endDate.date).toISOString()
|
? new Date(dateRange.endDate.date).toISOString()
|
||||||
: undefined,
|
: undefined,
|
||||||
...filters
|
...restFilters
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// axios serializes arrays as `ip[]=…`, which express's query
|
||||||
|
// parser does not read back as `ip`, so pass them in the URL
|
||||||
|
const sp = new URLSearchParams((ip ?? []).map((ip) => ["ip", ip]));
|
||||||
|
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/org/${orgId}/logs/request/export`,
|
`/org/${orgId}/logs/request/export?${sp.toString()}`,
|
||||||
{
|
{
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
params
|
params
|
||||||
|
|||||||
+7
-3
@@ -807,7 +807,8 @@ export const accessLogsFiltersSchema = z.object({
|
|||||||
action: z.string().optional().catch(undefined),
|
action: z.string().optional().catch(undefined),
|
||||||
location: z.string().optional().catch(undefined),
|
location: z.string().optional().catch(undefined),
|
||||||
actor: z.string().optional().catch(undefined),
|
actor: z.string().optional().catch(undefined),
|
||||||
type: z.string().optional().catch(undefined)
|
type: z.string().optional().catch(undefined),
|
||||||
|
ip: z.array(z.string()).optional().catch(undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AccessLogFilters = z.output<typeof accessLogsFiltersSchema>;
|
export type AccessLogFilters = z.output<typeof accessLogsFiltersSchema>;
|
||||||
@@ -932,10 +933,13 @@ export const logQueries = {
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["ACCESS_LOGS", orgId, "ALL", filters] as const,
|
queryKey: ["ACCESS_LOGS", orgId, "ALL", filters] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const { page, pageSize, ...rest } = filters;
|
const { page, pageSize, ip, ...rest } = filters;
|
||||||
|
const sp = new URLSearchParams(
|
||||||
|
(ip ?? []).map((ip) => ["ip", ip])
|
||||||
|
);
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<QueryAccessAuditLogResponse>
|
AxiosResponse<QueryAccessAuditLogResponse>
|
||||||
>(`/org/${orgId}/logs/access`, {
|
>(`/org/${orgId}/logs/access?${sp.toString()}`, {
|
||||||
params: {
|
params: {
|
||||||
...rest,
|
...rest,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
|
|||||||
Reference in New Issue
Block a user