Improve the frontend 5
This commit is contained in:
115
frontend/src/api/hooks/auditLogs.ts
Normal file
115
frontend/src/api/hooks/auditLogs.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Audit Logs React Query hooks
|
||||
*
|
||||
* Provides React Query hooks for fetching and managing audit logs
|
||||
* across all microservices with caching and real-time updates.
|
||||
*
|
||||
* Last Updated: 2025-11-02
|
||||
* Status: ✅ Complete
|
||||
*/
|
||||
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { auditLogsService } from '../services/auditLogs';
|
||||
import {
|
||||
AuditLogResponse,
|
||||
AuditLogFilters,
|
||||
AuditLogListResponse,
|
||||
AuditLogStatsResponse,
|
||||
AggregatedAuditLog,
|
||||
AuditLogServiceName,
|
||||
} from '../types/auditLogs';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
// Query Keys
|
||||
export const auditLogKeys = {
|
||||
all: ['audit-logs'] as const,
|
||||
lists: () => [...auditLogKeys.all, 'list'] as const,
|
||||
list: (tenantId: string, filters?: AuditLogFilters) =>
|
||||
[...auditLogKeys.lists(), tenantId, filters] as const,
|
||||
serviceList: (tenantId: string, service: AuditLogServiceName, filters?: AuditLogFilters) =>
|
||||
[...auditLogKeys.lists(), 'service', tenantId, service, filters] as const,
|
||||
stats: () => [...auditLogKeys.all, 'stats'] as const,
|
||||
stat: (tenantId: string, filters?: { start_date?: string; end_date?: string }) =>
|
||||
[...auditLogKeys.stats(), tenantId, filters] as const,
|
||||
serviceStat: (
|
||||
tenantId: string,
|
||||
service: AuditLogServiceName,
|
||||
filters?: { start_date?: string; end_date?: string }
|
||||
) => [...auditLogKeys.stats(), 'service', tenantId, service, filters] as const,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Hook to fetch audit logs from a single service
|
||||
*/
|
||||
export function useServiceAuditLogs(
|
||||
tenantId: string,
|
||||
serviceName: AuditLogServiceName,
|
||||
filters?: AuditLogFilters,
|
||||
options?: Omit<UseQueryOptions<AuditLogListResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery<AuditLogListResponse, ApiError>({
|
||||
queryKey: auditLogKeys.serviceList(tenantId, serviceName, filters),
|
||||
queryFn: () => auditLogsService.getServiceAuditLogs(tenantId, serviceName, filters),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch aggregated audit logs from ALL services
|
||||
*/
|
||||
export function useAllAuditLogs(
|
||||
tenantId: string,
|
||||
filters?: AuditLogFilters,
|
||||
options?: Omit<UseQueryOptions<AggregatedAuditLog[], ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery<AggregatedAuditLog[], ApiError>({
|
||||
queryKey: auditLogKeys.list(tenantId, filters),
|
||||
queryFn: () => auditLogsService.getAllAuditLogs(tenantId, filters),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 30000, // 30 seconds
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch audit log statistics from a single service
|
||||
*/
|
||||
export function useServiceAuditLogStats(
|
||||
tenantId: string,
|
||||
serviceName: AuditLogServiceName,
|
||||
filters?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
},
|
||||
options?: Omit<UseQueryOptions<AuditLogStatsResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery<AuditLogStatsResponse, ApiError>({
|
||||
queryKey: auditLogKeys.serviceStat(tenantId, serviceName, filters),
|
||||
queryFn: () => auditLogsService.getServiceAuditLogStats(tenantId, serviceName, filters),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 60000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch aggregated audit log statistics from ALL services
|
||||
*/
|
||||
export function useAllAuditLogStats(
|
||||
tenantId: string,
|
||||
filters?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
},
|
||||
options?: Omit<UseQueryOptions<AuditLogStatsResponse, ApiError>, 'queryKey' | 'queryFn'>
|
||||
) {
|
||||
return useQuery<AuditLogStatsResponse, ApiError>({
|
||||
queryKey: auditLogKeys.stat(tenantId, filters),
|
||||
queryFn: () => auditLogsService.getAllAuditLogStats(tenantId, filters),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 60000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
}
|
||||
267
frontend/src/api/services/auditLogs.ts
Normal file
267
frontend/src/api/services/auditLogs.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
// ================================================================
|
||||
// frontend/src/api/services/auditLogs.ts
|
||||
// ================================================================
|
||||
/**
|
||||
* Audit Logs Aggregation Service
|
||||
*
|
||||
* Aggregates audit logs from all microservices and provides
|
||||
* unified access to system event history.
|
||||
*
|
||||
* Backend endpoints:
|
||||
* - GET /tenants/{tenant_id}/{service}/audit-logs
|
||||
* - GET /tenants/{tenant_id}/{service}/audit-logs/stats
|
||||
*
|
||||
* Last Updated: 2025-11-02
|
||||
* Status: ✅ Complete - Multi-service aggregation
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client';
|
||||
import {
|
||||
AuditLogResponse,
|
||||
AuditLogFilters,
|
||||
AuditLogListResponse,
|
||||
AuditLogStatsResponse,
|
||||
AggregatedAuditLog,
|
||||
AUDIT_LOG_SERVICES,
|
||||
AuditLogServiceName,
|
||||
} from '../types/auditLogs';
|
||||
|
||||
export class AuditLogsService {
|
||||
private readonly baseUrl = '/tenants';
|
||||
|
||||
/**
|
||||
* Get audit logs from a single service
|
||||
*/
|
||||
async getServiceAuditLogs(
|
||||
tenantId: string,
|
||||
serviceName: AuditLogServiceName,
|
||||
filters?: AuditLogFilters
|
||||
): Promise<AuditLogListResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (filters?.start_date) queryParams.append('start_date', filters.start_date);
|
||||
if (filters?.end_date) queryParams.append('end_date', filters.end_date);
|
||||
if (filters?.user_id) queryParams.append('user_id', filters.user_id);
|
||||
if (filters?.action) queryParams.append('action', filters.action);
|
||||
if (filters?.resource_type) queryParams.append('resource_type', filters.resource_type);
|
||||
if (filters?.severity) queryParams.append('severity', filters.severity);
|
||||
if (filters?.search) queryParams.append('search', filters.search);
|
||||
if (filters?.limit) queryParams.append('limit', filters.limit.toString());
|
||||
if (filters?.offset) queryParams.append('offset', filters.offset.toString());
|
||||
|
||||
const url = `${this.baseUrl}/${tenantId}/${serviceName}/audit-logs${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||||
|
||||
return apiClient.get<AuditLogListResponse>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit log statistics from a single service
|
||||
*/
|
||||
async getServiceAuditLogStats(
|
||||
tenantId: string,
|
||||
serviceName: AuditLogServiceName,
|
||||
filters?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
): Promise<AuditLogStatsResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (filters?.start_date) queryParams.append('start_date', filters.start_date);
|
||||
if (filters?.end_date) queryParams.append('end_date', filters.end_date);
|
||||
|
||||
const url = `${this.baseUrl}/${tenantId}/${serviceName}/audit-logs/stats${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||||
|
||||
return apiClient.get<AuditLogStatsResponse>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated audit logs from ALL services
|
||||
* Makes parallel requests to all services and combines results
|
||||
*/
|
||||
async getAllAuditLogs(
|
||||
tenantId: string,
|
||||
filters?: AuditLogFilters
|
||||
): Promise<AggregatedAuditLog[]> {
|
||||
// Make parallel requests to all services
|
||||
const promises = AUDIT_LOG_SERVICES.map(service =>
|
||||
this.getServiceAuditLogs(tenantId, service, {
|
||||
...filters,
|
||||
limit: filters?.limit || 100,
|
||||
}).catch(error => {
|
||||
// If a service fails, log the error but don't fail the entire request
|
||||
console.warn(`Failed to fetch audit logs from ${service}:`, error);
|
||||
return { items: [], total: 0, limit: 0, offset: 0, has_more: false };
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// Combine all results
|
||||
const allLogs: AggregatedAuditLog[] = results.flatMap(result => result.items);
|
||||
|
||||
// Sort by created_at descending (most recent first)
|
||||
allLogs.sort((a, b) => {
|
||||
const dateA = new Date(a.created_at).getTime();
|
||||
const dateB = new Date(b.created_at).getTime();
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
// Apply limit if specified
|
||||
const limit = filters?.limit || 100;
|
||||
const offset = filters?.offset || 0;
|
||||
|
||||
return allLogs.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated statistics from ALL services
|
||||
*/
|
||||
async getAllAuditLogStats(
|
||||
tenantId: string,
|
||||
filters?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
): Promise<AuditLogStatsResponse> {
|
||||
// Make parallel requests to all services
|
||||
const promises = AUDIT_LOG_SERVICES.map(service =>
|
||||
this.getServiceAuditLogStats(tenantId, service, filters).catch(error => {
|
||||
console.warn(`Failed to fetch audit log stats from ${service}:`, error);
|
||||
return {
|
||||
total_events: 0,
|
||||
events_by_action: {},
|
||||
events_by_severity: {},
|
||||
events_by_resource_type: {},
|
||||
date_range: { min: null, max: null },
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// Aggregate statistics
|
||||
const aggregated: AuditLogStatsResponse = {
|
||||
total_events: 0,
|
||||
events_by_action: {},
|
||||
events_by_severity: {},
|
||||
events_by_resource_type: {},
|
||||
date_range: { min: null, max: null },
|
||||
};
|
||||
|
||||
for (const result of results) {
|
||||
aggregated.total_events += result.total_events;
|
||||
|
||||
// Merge events_by_action
|
||||
for (const [action, count] of Object.entries(result.events_by_action)) {
|
||||
aggregated.events_by_action[action] = (aggregated.events_by_action[action] || 0) + count;
|
||||
}
|
||||
|
||||
// Merge events_by_severity
|
||||
for (const [severity, count] of Object.entries(result.events_by_severity)) {
|
||||
aggregated.events_by_severity[severity] = (aggregated.events_by_severity[severity] || 0) + count;
|
||||
}
|
||||
|
||||
// Merge events_by_resource_type
|
||||
for (const [resource, count] of Object.entries(result.events_by_resource_type)) {
|
||||
aggregated.events_by_resource_type[resource] = (aggregated.events_by_resource_type[resource] || 0) + count;
|
||||
}
|
||||
|
||||
// Update date range
|
||||
if (result.date_range.min) {
|
||||
if (!aggregated.date_range.min || result.date_range.min < aggregated.date_range.min) {
|
||||
aggregated.date_range.min = result.date_range.min;
|
||||
}
|
||||
}
|
||||
if (result.date_range.max) {
|
||||
if (!aggregated.date_range.max || result.date_range.max > aggregated.date_range.max) {
|
||||
aggregated.date_range.max = result.date_range.max;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export audit logs to CSV format
|
||||
*/
|
||||
exportToCSV(logs: AggregatedAuditLog[]): string {
|
||||
if (logs.length === 0) return '';
|
||||
|
||||
const headers = [
|
||||
'Timestamp',
|
||||
'Service',
|
||||
'User ID',
|
||||
'Action',
|
||||
'Resource Type',
|
||||
'Resource ID',
|
||||
'Severity',
|
||||
'Description',
|
||||
'IP Address',
|
||||
'Endpoint',
|
||||
'Method',
|
||||
];
|
||||
|
||||
const rows = logs.map(log => [
|
||||
log.created_at,
|
||||
log.service_name,
|
||||
log.user_id || '',
|
||||
log.action,
|
||||
log.resource_type,
|
||||
log.resource_id || '',
|
||||
log.severity,
|
||||
log.description,
|
||||
log.ip_address || '',
|
||||
log.endpoint || '',
|
||||
log.method || '',
|
||||
]);
|
||||
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map(row => row.map(cell => `"${cell}"`).join(',')),
|
||||
].join('\n');
|
||||
|
||||
return csvContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export audit logs to JSON format
|
||||
*/
|
||||
exportToJSON(logs: AggregatedAuditLog[]): string {
|
||||
return JSON.stringify(logs, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download audit logs as a file
|
||||
*/
|
||||
downloadAuditLogs(
|
||||
logs: AggregatedAuditLog[],
|
||||
format: 'csv' | 'json',
|
||||
filename?: string
|
||||
): void {
|
||||
const content = format === 'csv' ? this.exportToCSV(logs) : this.exportToJSON(logs);
|
||||
const blob = new Blob([content], {
|
||||
type: format === 'csv' ? 'text/csv;charset=utf-8;' : 'application/json',
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute(
|
||||
'download',
|
||||
filename || `audit-logs-${new Date().toISOString().split('T')[0]}.${format}`
|
||||
);
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const auditLogsService = new AuditLogsService();
|
||||
84
frontend/src/api/types/auditLogs.ts
Normal file
84
frontend/src/api/types/auditLogs.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// ================================================================
|
||||
// frontend/src/api/types/auditLogs.ts
|
||||
// ================================================================
|
||||
/**
|
||||
* Audit Log Types - TypeScript interfaces for audit log data
|
||||
*
|
||||
* Aligned with backend schema:
|
||||
* - shared/models/audit_log_schemas.py
|
||||
*
|
||||
* Last Updated: 2025-11-02
|
||||
* Status: ✅ Complete - Aligned with backend
|
||||
*/
|
||||
|
||||
export interface AuditLogResponse {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
user_id: string | null;
|
||||
service_name: string;
|
||||
action: string;
|
||||
resource_type: string;
|
||||
resource_id: string | null;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
description: string;
|
||||
changes: Record<string, any> | null;
|
||||
audit_metadata: Record<string, any> | null;
|
||||
endpoint: string | null;
|
||||
method: string | null;
|
||||
ip_address: string | null;
|
||||
user_agent: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AuditLogFilters {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
user_id?: string;
|
||||
action?: string;
|
||||
resource_type?: string;
|
||||
severity?: 'low' | 'medium' | 'high' | 'critical';
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface AuditLogListResponse {
|
||||
items: AuditLogResponse[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface AuditLogStatsResponse {
|
||||
total_events: number;
|
||||
events_by_action: Record<string, number>;
|
||||
events_by_severity: Record<string, number>;
|
||||
events_by_resource_type: Record<string, number>;
|
||||
date_range: {
|
||||
min: string | null;
|
||||
max: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
// Aggregated audit log (combines logs from all services)
|
||||
export interface AggregatedAuditLog extends AuditLogResponse {
|
||||
// All fields from AuditLogResponse, service_name distinguishes the source
|
||||
}
|
||||
|
||||
// Service list for audit log aggregation
|
||||
export const AUDIT_LOG_SERVICES = [
|
||||
'sales',
|
||||
'inventory',
|
||||
'orders',
|
||||
'production',
|
||||
'recipes',
|
||||
'suppliers',
|
||||
'pos',
|
||||
'training',
|
||||
'notification',
|
||||
'external',
|
||||
'forecasting',
|
||||
] as const;
|
||||
|
||||
export type AuditLogServiceName = typeof AUDIT_LOG_SERVICES[number];
|
||||
Reference in New Issue
Block a user