feat: enhance audit logs with advanced filtering, CSV export, and role-based views
This commit is contained in:
parent
435375fc9f
commit
040d710ec7
@ -36,9 +36,18 @@ const stepSchema = z
|
|||||||
requires_signature: z.boolean().default(false),
|
requires_signature: z.boolean().default(false),
|
||||||
requires_comment: z.boolean().default(false),
|
requires_comment: z.boolean().default(false),
|
||||||
requires_attachment: z.boolean().default(false),
|
requires_attachment: z.boolean().default(false),
|
||||||
sla_hours: z.number().optional().nullable(),
|
sla_hours: z.preprocess(
|
||||||
sla_warning_hours: z.number().optional().nullable(),
|
(v) => (v === "" || v === null || Number.isNaN(v) ? undefined : v),
|
||||||
sla_escalation_hours: z.number().optional().nullable(),
|
z.number().int().nonnegative().optional().nullable(),
|
||||||
|
),
|
||||||
|
sla_warning_hours: z.preprocess(
|
||||||
|
(v) => (v === "" || v === null || Number.isNaN(v) ? undefined : v),
|
||||||
|
z.number().int().nonnegative().optional().nullable(),
|
||||||
|
),
|
||||||
|
sla_escalation_hours: z.preprocess(
|
||||||
|
(v) => (v === "" || v === null || Number.isNaN(v) ? undefined : v),
|
||||||
|
z.number().int().nonnegative().optional().nullable(),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
// Skip all assignee/action validation for terminal steps
|
// Skip all assignee/action validation for terminal steps
|
||||||
@ -428,7 +437,7 @@ export const WorkflowDefinitionModal = ({
|
|||||||
|
|
||||||
// Clean up SLA fields
|
// Clean up SLA fields
|
||||||
['sla_hours', 'sla_warning_hours', 'sla_escalation_hours'].forEach(field => {
|
['sla_hours', 'sla_warning_hours', 'sla_escalation_hours'].forEach(field => {
|
||||||
if (cleanedStep[field] === 0 || cleanedStep[field] === null || cleanedStep[field] === "") {
|
if (cleanedStep[field] === 0 || cleanedStep[field] === null || cleanedStep[field] === "" || Number.isNaN(cleanedStep[field])) {
|
||||||
delete cleanedStep[field];
|
delete cleanedStep[field];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,7 +11,9 @@ import {
|
|||||||
} from '@/components/shared';
|
} from '@/components/shared';
|
||||||
import { Download, ArrowUpDown } from 'lucide-react';
|
import { Download, ArrowUpDown } from 'lucide-react';
|
||||||
import { auditLogService } from '@/services/audit-log-service';
|
import { auditLogService } from '@/services/audit-log-service';
|
||||||
|
import { tenantService } from '@/services/tenant-service';
|
||||||
import type { AuditLog } from '@/types/audit-log';
|
import type { AuditLog } from '@/types/audit-log';
|
||||||
|
import type { Tenant } from '@/types/tenant';
|
||||||
|
|
||||||
// Helper function to format date
|
// Helper function to format date
|
||||||
const formatDate = (dateString: string): string => {
|
const formatDate = (dateString: string): string => {
|
||||||
@ -57,12 +59,13 @@ const getStatusColor = (status: number | null): string => {
|
|||||||
|
|
||||||
const AuditLogs = (): ReactElement => {
|
const AuditLogs = (): ReactElement => {
|
||||||
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
||||||
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [limit, setLimit] = useState<number>(5);
|
const [limit, setLimit] = useState<number>(10);
|
||||||
const [pagination, setPagination] = useState<{
|
const [pagination, setPagination] = useState<{
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
@ -71,30 +74,57 @@ const AuditLogs = (): ReactElement => {
|
|||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
}>({
|
}>({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 5,
|
limit: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
|
const [tenantFilter, setTenantFilter] = useState<string | null>(null);
|
||||||
const [methodFilter, setMethodFilter] = useState<string | null>(null);
|
const [methodFilter, setMethodFilter] = useState<string | null>(null);
|
||||||
|
const [actionFilter, setActionFilter] = useState<string | null>(null);
|
||||||
|
const [resourceTypeFilter, setResourceTypeFilter] = useState<string | null>(null);
|
||||||
|
const [startDate, setStartDate] = useState<string>('');
|
||||||
|
const [endDate, setEndDate] = useState<string>('');
|
||||||
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
||||||
|
|
||||||
// View modal
|
// View modal
|
||||||
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
||||||
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(null);
|
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchAuditLogs = async (
|
// Fetch tenants on mount for the selector
|
||||||
page: number,
|
useEffect(() => {
|
||||||
itemsPerPage: number,
|
const fetchTenants = async () => {
|
||||||
method: string | null = null,
|
try {
|
||||||
sortBy: string[] | null = null
|
const response = await tenantService.getAll(1, 100);
|
||||||
): Promise<void> => {
|
if (response.success) {
|
||||||
|
setTenants(response.data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load tenants:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchTenants();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchAuditLogs = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await auditLogService.getAll(page, itemsPerPage, method, sortBy);
|
const response = await auditLogService.getAll(
|
||||||
|
currentPage,
|
||||||
|
limit,
|
||||||
|
{
|
||||||
|
tenant_id: tenantFilter,
|
||||||
|
method: methodFilter,
|
||||||
|
action: actionFilter,
|
||||||
|
resource_type: resourceTypeFilter,
|
||||||
|
startDate: startDate || null,
|
||||||
|
endDate: endDate || null,
|
||||||
|
},
|
||||||
|
orderBy
|
||||||
|
);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setAuditLogs(response.data);
|
setAuditLogs(response.data);
|
||||||
setPagination(response.pagination);
|
setPagination(response.pagination);
|
||||||
@ -110,8 +140,32 @@ const AuditLogs = (): ReactElement => {
|
|||||||
|
|
||||||
// Fetch audit logs on mount and when pagination/filters change
|
// Fetch audit logs on mount and when pagination/filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAuditLogs(currentPage, limit, methodFilter, orderBy);
|
fetchAuditLogs();
|
||||||
}, [currentPage, limit, methodFilter, orderBy]);
|
}, [currentPage, limit, tenantFilter, methodFilter, actionFilter, resourceTypeFilter, startDate, endDate, orderBy]);
|
||||||
|
|
||||||
|
// Handle Export
|
||||||
|
const handleExport = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const response = await auditLogService.export({
|
||||||
|
format: 'json',
|
||||||
|
startDate: startDate || undefined,
|
||||||
|
endDate: endDate || undefined,
|
||||||
|
tenantId: tenantFilter || undefined
|
||||||
|
});
|
||||||
|
if (response.success) {
|
||||||
|
const blob = new Blob([JSON.stringify(response.data.records, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `platform-audit-logs-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
alert('Export failed: ' + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// View audit log handler
|
// View audit log handler
|
||||||
const handleViewAuditLog = (auditLogId: string): void => {
|
const handleViewAuditLog = (auditLogId: string): void => {
|
||||||
@ -131,10 +185,19 @@ const AuditLogs = (): ReactElement => {
|
|||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
label: 'Timestamp',
|
label: 'Timestamp',
|
||||||
render: (log) => (
|
render: (log) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">{formatDate(log.created_at)}</span>
|
<span className="text-sm font-normal text-[#0f1724] whitespace-nowrap">{formatDate(log.created_at)}</span>
|
||||||
),
|
),
|
||||||
mobileLabel: 'Time',
|
mobileLabel: 'Time',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'tenant',
|
||||||
|
label: 'Tenant',
|
||||||
|
render: (log) => (
|
||||||
|
<span className="text-sm font-normal text-[#0f1724]">
|
||||||
|
{log.tenant ? log.tenant.name : 'N/A'}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'resource_type',
|
key: 'resource_type',
|
||||||
label: 'Resource Type',
|
label: 'Resource Type',
|
||||||
@ -151,21 +214,12 @@ const AuditLogs = (): ReactElement => {
|
|||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'resource_id',
|
|
||||||
label: 'Resource ID',
|
|
||||||
render: (log) => (
|
|
||||||
<span className="text-sm font-normal text-[#0f1724] font-mono truncate max-w-[150px]">
|
|
||||||
{log.resource_id || 'N/A'}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'user',
|
key: 'user',
|
||||||
label: 'User',
|
label: 'User',
|
||||||
render: (log) => (
|
render: (log) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="text-sm font-normal text-[#0f1724]">
|
||||||
{log.user ? `${log.user.first_name} ${log.user.last_name}` : 'N/A'}
|
{log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -187,15 +241,6 @@ const AuditLogs = (): ReactElement => {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'ip_address',
|
|
||||||
label: 'IP Address',
|
|
||||||
render: (log) => (
|
|
||||||
<span className="text-sm font-normal text-[#0f1724] font-mono">
|
|
||||||
{log.ip_address || 'N/A'}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
label: 'Actions',
|
label: 'Actions',
|
||||||
@ -220,6 +265,7 @@ const AuditLogs = (): ReactElement => {
|
|||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="text-sm font-medium text-[#0f1724] truncate">{log.resource_type}</h3>
|
<h3 className="text-sm font-medium text-[#0f1724] truncate">{log.resource_type}</h3>
|
||||||
|
<p className="text-xs text-[#9aa6b2] mb-1">{log.tenant ? log.tenant.name : 'System'}</p>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<StatusBadge variant={getActionVariant(log.action)}>
|
<StatusBadge variant={getActionVariant(log.action)}>
|
||||||
{log.action}
|
{log.action}
|
||||||
@ -242,33 +288,15 @@ const AuditLogs = (): ReactElement => {
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-[#9aa6b2]">User:</span>
|
<span className="text-[#9aa6b2]">User:</span>
|
||||||
<p className="text-[#0f1724] font-normal mt-1">
|
<p className="text-[#0f1724] font-normal mt-1">
|
||||||
{log.user ? `${log.user.first_name} ${log.user.last_name}` : 'N/A'}
|
{log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span className="text-[#9aa6b2]">Method:</span>
|
|
||||||
<div className="mt-1">
|
|
||||||
<StatusBadge variant={getMethodVariant(log.request_method)}>
|
|
||||||
{log.request_method ? log.request_method.toUpperCase() : 'N/A'}
|
|
||||||
</StatusBadge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[#9aa6b2]">Status:</span>
|
<span className="text-[#9aa6b2]">Status:</span>
|
||||||
<p className={`font-normal mt-1 ${getStatusColor(log.response_status)}`}>
|
<p className={`font-normal mt-1 ${getStatusColor(log.response_status)}`}>
|
||||||
{log.response_status || 'N/A'}
|
{log.response_status || 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span className="text-[#9aa6b2]">Resource ID:</span>
|
|
||||||
<p className="text-[#0f1724] font-normal mt-1 font-mono truncate">
|
|
||||||
{log.resource_id || 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-[#9aa6b2]">IP Address:</span>
|
|
||||||
<p className="text-[#0f1724] font-normal mt-1 font-mono">{log.ip_address || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -277,66 +305,152 @@ const AuditLogs = (): ReactElement => {
|
|||||||
<Layout
|
<Layout
|
||||||
currentPage="Audit Logs"
|
currentPage="Audit Logs"
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: 'Audit Logs',
|
title: 'Platform Audit Logs',
|
||||||
description: 'View and manage all audit logs in the QAssure platform.',
|
description: 'Global monitoring of all actions, logins, and changes across all tenants in the platform.',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
||||||
{/* Table Header with Filters */}
|
{/* Table Header with Filters */}
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col gap-4">
|
||||||
{/* Filters */}
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
{/* Filters */}
|
||||||
{/* Method Filter */}
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<FilterDropdown
|
{/* Tenant Selector */}
|
||||||
label="Method"
|
<FilterDropdown
|
||||||
options={[
|
label="Tenant"
|
||||||
{ value: 'GET', label: 'GET' },
|
options={tenants.map(t => ({ value: t.id, label: t.name }))}
|
||||||
{ value: 'POST', label: 'POST' },
|
value={tenantFilter}
|
||||||
{ value: 'PUT', label: 'PUT' },
|
onChange={(value) => {
|
||||||
{ value: 'DELETE', label: 'DELETE' },
|
setTenantFilter(value as string | null);
|
||||||
{ value: 'PATCH', label: 'PATCH' },
|
setCurrentPage(1);
|
||||||
]}
|
}}
|
||||||
value={methodFilter}
|
placeholder="All Tenants"
|
||||||
onChange={(value) => {
|
/>
|
||||||
setMethodFilter(value as string | null);
|
|
||||||
setCurrentPage(1);
|
{/* Action Filter */}
|
||||||
}}
|
<FilterDropdown
|
||||||
placeholder="All"
|
label="Action"
|
||||||
/>
|
options={[
|
||||||
|
{ value: 'LOGIN', label: 'LOGIN' },
|
||||||
|
{ value: 'CREATE', label: 'CREATE' },
|
||||||
|
{ value: 'UPDATE', label: 'UPDATE' },
|
||||||
|
{ value: 'DELETE', label: 'DELETE' },
|
||||||
|
{ value: 'SUBMIT', label: 'SUBMIT' },
|
||||||
|
]}
|
||||||
|
value={actionFilter}
|
||||||
|
onChange={(value) => {
|
||||||
|
setActionFilter(value as string | null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="All Actions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Resource Filter */}
|
||||||
|
<FilterDropdown
|
||||||
|
label="Resource"
|
||||||
|
options={[
|
||||||
|
{ value: 'document', label: 'Document' },
|
||||||
|
{ value: 'user', label: 'User' },
|
||||||
|
{ value: 'tenant', label: 'Tenant' },
|
||||||
|
{ value: 'role', label: 'Role' },
|
||||||
|
]}
|
||||||
|
value={resourceTypeFilter}
|
||||||
|
onChange={(value) => {
|
||||||
|
setResourceTypeFilter(value as string | null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="All Resources"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Method Filter */}
|
||||||
|
<FilterDropdown
|
||||||
|
label="Method"
|
||||||
|
options={[
|
||||||
|
{ value: 'GET', label: 'GET' },
|
||||||
|
{ value: 'POST', label: 'POST' },
|
||||||
|
{ value: 'PUT', label: 'PUT' },
|
||||||
|
{ value: 'DELETE', label: 'DELETE' },
|
||||||
|
]}
|
||||||
|
value={methodFilter}
|
||||||
|
onChange={(value) => {
|
||||||
|
setMethodFilter(value as string | null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="All Methods"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleExport}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Download className="w-3.5 h-3.5" />
|
||||||
|
<span>Export Platform Logs</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-3 md:gap-6 pt-1 border-t border-gray-50 mt-1 pt-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium text-[#475569]">Start Date:</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => {
|
||||||
|
setStartDate(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium text-[#475569]">End Date:</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEndDate(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Sort Filter */}
|
|
||||||
<FilterDropdown
|
<FilterDropdown
|
||||||
label="Sort by"
|
label="Sort"
|
||||||
options={[
|
options={[
|
||||||
{ value: ['created_at', 'desc'], label: 'Newest First' },
|
{ value: ['created_at', 'desc'], label: 'Newest First' },
|
||||||
{ value: ['created_at', 'asc'], label: 'Oldest First' },
|
{ value: ['created_at', 'asc'], label: 'Oldest First' },
|
||||||
{ value: ['action', 'asc'], label: 'Action (A-Z)' },
|
|
||||||
{ value: ['action', 'desc'], label: 'Action (Z-A)' },
|
|
||||||
{ value: ['resource_type', 'asc'], label: 'Resource Type (A-Z)' },
|
|
||||||
{ value: ['resource_type', 'desc'], label: 'Resource Type (Z-A)' },
|
|
||||||
]}
|
]}
|
||||||
value={orderBy}
|
value={orderBy}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setOrderBy(value as string[] | null);
|
setOrderBy(value as string[] | null);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
placeholder="Default"
|
placeholder="Newest"
|
||||||
showIcon
|
showIcon
|
||||||
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
|
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{(startDate || endDate || tenantFilter || actionFilter || resourceTypeFilter) && (
|
||||||
<div className="flex items-center gap-2">
|
<button
|
||||||
{/* Export Button */}
|
onClick={() => {
|
||||||
<button
|
setStartDate('');
|
||||||
type="button"
|
setEndDate('');
|
||||||
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
|
setTenantFilter(null);
|
||||||
>
|
setActionFilter(null);
|
||||||
<Download className="w-3.5 h-3.5" />
|
setResourceTypeFilter(null);
|
||||||
<span>Export</span>
|
setCurrentPage(1);
|
||||||
</button>
|
}}
|
||||||
|
className="text-xs text-[#ef4444] hover:underline ml-auto"
|
||||||
|
>
|
||||||
|
Reset All Filters
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -348,7 +462,7 @@ const AuditLogs = (): ReactElement => {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
mobileCardRenderer={mobileCardRenderer}
|
mobileCardRenderer={mobileCardRenderer}
|
||||||
emptyMessage="No audit logs found"
|
emptyMessage="No platform-wide audit logs found"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
|
|||||||
@ -57,14 +57,17 @@ const getStatusColor = (status: number | null): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AuditLogs = (): ReactElement => {
|
const AuditLogs = (): ReactElement => {
|
||||||
|
const roles = useAppSelector((state) => state.auth.roles);
|
||||||
const tenantId = useAppSelector((state) => state.auth.tenantId);
|
const tenantId = useAppSelector((state) => state.auth.tenantId);
|
||||||
|
const isTenantAdmin = roles?.includes('tenant_admin');
|
||||||
|
|
||||||
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [limit, setLimit] = useState<number>(5);
|
const [limit, setLimit] = useState<number>(10);
|
||||||
const [pagination, setPagination] = useState<{
|
const [pagination, setPagination] = useState<{
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
@ -73,7 +76,7 @@ const AuditLogs = (): ReactElement => {
|
|||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
}>({
|
}>({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 5,
|
limit: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
@ -81,22 +84,40 @@ const AuditLogs = (): ReactElement => {
|
|||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [methodFilter, setMethodFilter] = useState<string | null>(null);
|
const [methodFilter, setMethodFilter] = useState<string | null>(null);
|
||||||
|
const [actionFilter, setActionFilter] = useState<string | null>(null);
|
||||||
|
const [resourceTypeFilter, setResourceTypeFilter] = useState<string | null>(null);
|
||||||
|
const [startDate, setStartDate] = useState<string>('');
|
||||||
|
const [endDate, setEndDate] = useState<string>('');
|
||||||
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
||||||
|
|
||||||
// View modal
|
// View modal
|
||||||
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
||||||
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(null);
|
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchAuditLogs = async (
|
const fetchAuditLogs = async (): Promise<void> => {
|
||||||
page: number,
|
|
||||||
itemsPerPage: number,
|
|
||||||
method: string | null = null,
|
|
||||||
sortBy: string[] | null = null
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await auditLogService.getAll(page, itemsPerPage, method, sortBy, tenantId);
|
|
||||||
|
let response;
|
||||||
|
if (isTenantAdmin) {
|
||||||
|
response = await auditLogService.getAll(
|
||||||
|
currentPage,
|
||||||
|
limit,
|
||||||
|
{
|
||||||
|
method: methodFilter,
|
||||||
|
action: actionFilter,
|
||||||
|
resource_type: resourceTypeFilter,
|
||||||
|
startDate: startDate || null,
|
||||||
|
endDate: endDate || null,
|
||||||
|
tenant_id: tenantId
|
||||||
|
},
|
||||||
|
orderBy
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
response = await auditLogService.getMyLogs(currentPage, limit);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setAuditLogs(response.data);
|
setAuditLogs(response.data);
|
||||||
setPagination(response.pagination);
|
setPagination(response.pagination);
|
||||||
@ -112,10 +133,8 @@ const AuditLogs = (): ReactElement => {
|
|||||||
|
|
||||||
// Fetch audit logs on mount and when pagination/filters change
|
// Fetch audit logs on mount and when pagination/filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tenantId) {
|
fetchAuditLogs();
|
||||||
fetchAuditLogs(currentPage, limit, methodFilter, orderBy);
|
}, [currentPage, limit, methodFilter, actionFilter, resourceTypeFilter, startDate, endDate, orderBy, tenantId]);
|
||||||
}
|
|
||||||
}, [currentPage, limit, methodFilter, orderBy, tenantId]);
|
|
||||||
|
|
||||||
// View audit log handler
|
// View audit log handler
|
||||||
const handleViewAuditLog = (auditLogId: string): void => {
|
const handleViewAuditLog = (auditLogId: string): void => {
|
||||||
@ -123,6 +142,34 @@ const AuditLogs = (): ReactElement => {
|
|||||||
setViewModalOpen(true);
|
setViewModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export handler
|
||||||
|
const handleExport = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const response = await auditLogService.export({
|
||||||
|
format: 'json',
|
||||||
|
startDate: startDate || undefined,
|
||||||
|
endDate: endDate || undefined,
|
||||||
|
tenantId: tenantId || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// In a real app, we'd trigger a file download here.
|
||||||
|
// For now, we'll just log and show a message since the response is JSON.
|
||||||
|
console.log('Export data:', response.data.records);
|
||||||
|
const blob = new Blob([JSON.stringify(response.data.records, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `audit-logs-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
alert('Failed to export audit logs: ' + (err.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Load audit log for view
|
// Load audit log for view
|
||||||
const loadAuditLog = async (id: string): Promise<AuditLog> => {
|
const loadAuditLog = async (id: string): Promise<AuditLog> => {
|
||||||
const response = await auditLogService.getById(id);
|
const response = await auditLogService.getById(id);
|
||||||
@ -135,7 +182,7 @@ const AuditLogs = (): ReactElement => {
|
|||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
label: 'Timestamp',
|
label: 'Timestamp',
|
||||||
render: (log) => (
|
render: (log) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">{formatDate(log.created_at)}</span>
|
<span className="text-sm font-normal text-[#0f1724] whitespace-nowrap">{formatDate(log.created_at)}</span>
|
||||||
),
|
),
|
||||||
mobileLabel: 'Time',
|
mobileLabel: 'Time',
|
||||||
},
|
},
|
||||||
@ -155,21 +202,12 @@ const AuditLogs = (): ReactElement => {
|
|||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'resource_id',
|
|
||||||
label: 'Resource ID',
|
|
||||||
render: (log) => (
|
|
||||||
<span className="text-sm font-normal text-[#0f1724] font-mono truncate max-w-[150px]">
|
|
||||||
{log.resource_id || 'N/A'}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'user',
|
key: 'user',
|
||||||
label: 'User',
|
label: 'User',
|
||||||
render: (log) => (
|
render: (log) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="text-sm font-normal text-[#0f1724]">
|
||||||
{log.user ? `${log.user.first_name} ${log.user.last_name}` : 'N/A'}
|
{log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -246,7 +284,7 @@ const AuditLogs = (): ReactElement => {
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-[#9aa6b2]">User:</span>
|
<span className="text-[#9aa6b2]">User:</span>
|
||||||
<p className="text-[#0f1724] font-normal mt-1">
|
<p className="text-[#0f1724] font-normal mt-1">
|
||||||
{log.user ? `${log.user.first_name} ${log.user.last_name}` : 'N/A'}
|
{log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -263,16 +301,6 @@ const AuditLogs = (): ReactElement => {
|
|||||||
{log.response_status || 'N/A'}
|
{log.response_status || 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span className="text-[#9aa6b2]">Resource ID:</span>
|
|
||||||
<p className="text-[#0f1724] font-normal mt-1 font-mono truncate">
|
|
||||||
{log.resource_id || 'N/A'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-[#9aa6b2]">IP Address:</span>
|
|
||||||
<p className="text-[#0f1724] font-normal mt-1 font-mono">{log.ip_address || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -281,66 +309,155 @@ const AuditLogs = (): ReactElement => {
|
|||||||
<Layout
|
<Layout
|
||||||
currentPage="Audit Logs"
|
currentPage="Audit Logs"
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: 'Audit Logs',
|
title: isTenantAdmin ? 'System Audit Logs' : 'My Activity',
|
||||||
description: 'View and manage all audit logs in the QAssure platform.',
|
description: isTenantAdmin
|
||||||
|
? 'Monitor all activities and changes across the quality platform.'
|
||||||
|
: 'View a chronological history of your own actions on the platform.',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
||||||
{/* Table Header with Filters */}
|
{/* Table Header with Filters */}
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col gap-4">
|
||||||
{/* Filters */}
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
{/* Filters */}
|
||||||
{/* Method Filter */}
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<FilterDropdown
|
{isTenantAdmin && (
|
||||||
label="Method"
|
<>
|
||||||
options={[
|
{/* Action Filter */}
|
||||||
{ value: 'GET', label: 'GET' },
|
<FilterDropdown
|
||||||
{ value: 'POST', label: 'POST' },
|
label="Action"
|
||||||
{ value: 'PUT', label: 'PUT' },
|
options={[
|
||||||
{ value: 'DELETE', label: 'DELETE' },
|
{ value: 'LOGIN', label: 'LOGIN' },
|
||||||
{ value: 'PATCH', label: 'PATCH' },
|
{ value: 'CREATE', label: 'CREATE' },
|
||||||
]}
|
{ value: 'UPDATE', label: 'UPDATE' },
|
||||||
value={methodFilter}
|
{ value: 'DELETE', label: 'DELETE' },
|
||||||
onChange={(value) => {
|
{ value: 'SUBMIT', label: 'SUBMIT' },
|
||||||
setMethodFilter(value as string | null);
|
{ value: 'APPROVE', label: 'APPROVE' },
|
||||||
setCurrentPage(1);
|
{ value: 'REJECT', label: 'REJECT' },
|
||||||
}}
|
]}
|
||||||
placeholder="All"
|
value={actionFilter}
|
||||||
/>
|
onChange={(value) => {
|
||||||
|
setActionFilter(value as string | null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="All Actions"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Sort Filter */}
|
{/* Resource Type Filter */}
|
||||||
<FilterDropdown
|
<FilterDropdown
|
||||||
label="Sort by"
|
label="Resource"
|
||||||
options={[
|
options={[
|
||||||
{ value: ['created_at', 'desc'], label: 'Newest First' },
|
{ value: 'document', label: 'Document' },
|
||||||
{ value: ['created_at', 'asc'], label: 'Oldest First' },
|
{ value: 'user', label: 'User' },
|
||||||
{ value: ['action', 'asc'], label: 'Action (A-Z)' },
|
{ value: 'capa', label: 'CAPA' },
|
||||||
{ value: ['action', 'desc'], label: 'Action (Z-A)' },
|
{ value: 'workflow', label: 'Workflow' },
|
||||||
{ value: ['resource_type', 'asc'], label: 'Resource Type (A-Z)' },
|
{ value: 'training', label: 'Training' },
|
||||||
{ value: ['resource_type', 'desc'], label: 'Resource Type (Z-A)' },
|
{ value: 'project', label: 'Project' },
|
||||||
]}
|
]}
|
||||||
value={orderBy}
|
value={resourceTypeFilter}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setOrderBy(value as string[] | null);
|
setResourceTypeFilter(value as string | null);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
placeholder="Default"
|
placeholder="All Resources"
|
||||||
showIcon
|
/>
|
||||||
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
|
|
||||||
/>
|
{/* Method Filter */}
|
||||||
|
<FilterDropdown
|
||||||
|
label="Method"
|
||||||
|
options={[
|
||||||
|
{ value: 'GET', label: 'GET' },
|
||||||
|
{ value: 'POST', label: 'POST' },
|
||||||
|
{ value: 'PUT', label: 'PUT' },
|
||||||
|
{ value: 'DELETE', label: 'DELETE' },
|
||||||
|
]}
|
||||||
|
value={methodFilter}
|
||||||
|
onChange={(value) => {
|
||||||
|
setMethodFilter(value as string | null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="All Methods"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sort Filter - Always show as it's useful for everyone */}
|
||||||
|
<FilterDropdown
|
||||||
|
label="Sort by"
|
||||||
|
options={[
|
||||||
|
{ value: ['created_at', 'desc'], label: 'Newest First' },
|
||||||
|
{ value: ['created_at', 'asc'], label: 'Oldest First' },
|
||||||
|
{ value: ['action', 'asc'], label: 'Action (A-Z)' },
|
||||||
|
{ value: ['resource_type', 'asc'], label: 'Resource Type (A-Z)' },
|
||||||
|
]}
|
||||||
|
value={orderBy}
|
||||||
|
onChange={(value) => {
|
||||||
|
setOrderBy(value as string[] | null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="Newest"
|
||||||
|
showIcon
|
||||||
|
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isTenantAdmin && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleExport}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Download className="w-3.5 h-3.5" />
|
||||||
|
<span>Export</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Date Filters - Separated row for better spacing */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-3 md:gap-6 pt-1">
|
||||||
{/* Export Button */}
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<span className="text-xs font-medium text-[#475569]">From:</span>
|
||||||
type="button"
|
<input
|
||||||
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
|
type="date"
|
||||||
>
|
value={startDate}
|
||||||
<Download className="w-3.5 h-3.5" />
|
onChange={(e) => {
|
||||||
<span>Export</span>
|
setStartDate(e.target.value);
|
||||||
</button>
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium text-[#475569]">To:</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEndDate(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{(startDate || endDate || actionFilter || resourceTypeFilter || methodFilter) && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setStartDate('');
|
||||||
|
setEndDate('');
|
||||||
|
setActionFilter(null);
|
||||||
|
setResourceTypeFilter(null);
|
||||||
|
setMethodFilter(null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="text-xs text-[#ef4444] hover:underline"
|
||||||
|
>
|
||||||
|
Clear all filters
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -352,7 +469,7 @@ const AuditLogs = (): ReactElement => {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
mobileCardRenderer={mobileCardRenderer}
|
mobileCardRenderer={mobileCardRenderer}
|
||||||
emptyMessage="No audit logs found"
|
emptyMessage={isTenantAdmin ? "No audit logs found matching your criteria" : "No activity recorded for your account yet"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
|
|||||||
@ -4,29 +4,76 @@ import type { AuditLogsResponse, GetAuditLogResponse } from '@/types/audit-log';
|
|||||||
export const auditLogService = {
|
export const auditLogService = {
|
||||||
getAll: async (
|
getAll: async (
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20,
|
limit: number = 100,
|
||||||
method?: string | null,
|
filters: {
|
||||||
orderBy?: string[] | null,
|
method?: string | null;
|
||||||
tenantId?: string | null
|
action?: string | null;
|
||||||
|
resource_type?: string | null;
|
||||||
|
user_id?: string | null;
|
||||||
|
response_status?: number | null;
|
||||||
|
tenant_id?: string | null;
|
||||||
|
startDate?: string | null;
|
||||||
|
endDate?: string | null;
|
||||||
|
} = {},
|
||||||
|
orderBy?: string[] | null
|
||||||
): Promise<AuditLogsResponse> => {
|
): Promise<AuditLogsResponse> => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append('page', String(page));
|
params.append('page', String(page));
|
||||||
params.append('limit', String(limit));
|
params.append('limit', String(limit));
|
||||||
if (method) {
|
|
||||||
params.append('method', method);
|
if (filters.method) params.append('method', filters.method);
|
||||||
}
|
if (filters.action) params.append('action', filters.action);
|
||||||
if (tenantId) {
|
if (filters.resource_type) params.append('resource_type', filters.resource_type);
|
||||||
params.append('tenant_id', tenantId);
|
if (filters.user_id) params.append('user_id', filters.user_id);
|
||||||
}
|
if (filters.response_status) params.append('response_status', String(filters.response_status));
|
||||||
|
if (filters.tenant_id) params.append('tenant_id', filters.tenant_id);
|
||||||
|
if (filters.startDate) params.append('startDate', filters.startDate);
|
||||||
|
if (filters.endDate) params.append('endDate', filters.endDate);
|
||||||
|
|
||||||
if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) {
|
if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) {
|
||||||
params.append('orderBy[]', orderBy[0]);
|
params.append('orderBy[]', orderBy[0]);
|
||||||
params.append('orderBy[]', orderBy[1]);
|
params.append('orderBy[]', orderBy[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiClient.get<AuditLogsResponse>(`/audit-logs?${params.toString()}`);
|
const response = await apiClient.get<AuditLogsResponse>(`/audit-logs?${params.toString()}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getMyLogs: async (page: number = 1, limit: number = 50): Promise<AuditLogsResponse> => {
|
||||||
|
const response = await apiClient.get<AuditLogsResponse>(
|
||||||
|
`/audit-logs/my-logs?page=${page}&limit=${limit}`
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
getById: async (id: string): Promise<GetAuditLogResponse> => {
|
getById: async (id: string): Promise<GetAuditLogResponse> => {
|
||||||
const response = await apiClient.get<GetAuditLogResponse>(`/audit-logs/${id}`);
|
const response = await apiClient.get<GetAuditLogResponse>(`/audit-logs/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getStats: async (params: { tenantId?: string; startDate?: string; endDate?: string } = {}): Promise<any> => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (params.tenantId) searchParams.append('tenantId', params.tenantId);
|
||||||
|
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||||
|
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||||
|
|
||||||
|
const response = await apiClient.get(`/audit-logs/stats?${searchParams.toString()}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
export: async (params: {
|
||||||
|
format?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
tenantId?: string;
|
||||||
|
} = {}): Promise<any> => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (params.format) searchParams.append('format', params.format);
|
||||||
|
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||||
|
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||||
|
if (params.tenantId) searchParams.append('tenantId', params.tenantId);
|
||||||
|
|
||||||
|
const response = await apiClient.get(`/audit-logs/export?${searchParams.toString()}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export interface AuditLog {
|
|||||||
metadata: Record<string, unknown> | null;
|
metadata: Record<string, unknown> | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
user_email: string | null;
|
||||||
user: AuditLogUser | null;
|
user: AuditLogUser | null;
|
||||||
tenant: AuditLogTenant | null;
|
tenant: AuditLogTenant | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user