-
This commit is contained in:
519
kms-frontend/src/components/Audit.tsx
Normal file
519
kms-frontend/src/components/Audit.tsx
Normal file
@ -0,0 +1,519 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Card,
|
||||
Typography,
|
||||
Space,
|
||||
Tag,
|
||||
DatePicker,
|
||||
Select,
|
||||
Input,
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
Alert,
|
||||
Timeline,
|
||||
} from 'antd';
|
||||
import {
|
||||
AuditOutlined,
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
UserOutlined,
|
||||
AppstoreOutlined,
|
||||
KeyOutlined,
|
||||
ClockCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
|
||||
interface AuditLogEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
user_id: string;
|
||||
action: string;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
status: 'success' | 'failure' | 'warning';
|
||||
ip_address: string;
|
||||
user_agent: string;
|
||||
details: Record<string, any>;
|
||||
}
|
||||
|
||||
// Mock audit data for demonstration
|
||||
const mockAuditData: AuditLogEntry[] = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: dayjs().subtract(1, 'hour').toISOString(),
|
||||
user_id: 'admin@example.com',
|
||||
action: 'CREATE_APPLICATION',
|
||||
resource_type: 'application',
|
||||
resource_id: 'com.example.newapp',
|
||||
status: 'success',
|
||||
ip_address: '192.168.1.100',
|
||||
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
details: {
|
||||
app_link: 'https://newapp.example.com',
|
||||
owner: 'Development Team'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: dayjs().subtract(2, 'hours').toISOString(),
|
||||
user_id: 'user@example.com',
|
||||
action: 'CREATE_TOKEN',
|
||||
resource_type: 'token',
|
||||
resource_id: 'token-abc123',
|
||||
status: 'success',
|
||||
ip_address: '192.168.1.101',
|
||||
user_agent: 'curl/7.68.0',
|
||||
details: {
|
||||
app_id: 'com.example.app',
|
||||
permissions: ['repo.read', 'repo.write']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: dayjs().subtract(3, 'hours').toISOString(),
|
||||
user_id: 'admin@example.com',
|
||||
action: 'DELETE_TOKEN',
|
||||
resource_type: 'token',
|
||||
resource_id: 'token-xyz789',
|
||||
status: 'success',
|
||||
ip_address: '192.168.1.100',
|
||||
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
details: {
|
||||
app_id: 'com.example.oldapp',
|
||||
reason: 'Token compromised'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
timestamp: dayjs().subtract(4, 'hours').toISOString(),
|
||||
user_id: 'user@example.com',
|
||||
action: 'VERIFY_TOKEN',
|
||||
resource_type: 'token',
|
||||
resource_id: 'token-def456',
|
||||
status: 'failure',
|
||||
ip_address: '192.168.1.102',
|
||||
user_agent: 'PostmanRuntime/7.28.4',
|
||||
details: {
|
||||
app_id: 'com.example.app',
|
||||
error: 'Token expired'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
timestamp: dayjs().subtract(6, 'hours').toISOString(),
|
||||
user_id: 'admin@example.com',
|
||||
action: 'UPDATE_APPLICATION',
|
||||
resource_type: 'application',
|
||||
resource_id: 'com.example.app',
|
||||
status: 'success',
|
||||
ip_address: '192.168.1.100',
|
||||
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
details: {
|
||||
changes: {
|
||||
callback_url: 'https://updated.example.com/callback'
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const Audit: React.FC = () => {
|
||||
const [auditData, setAuditData] = useState<AuditLogEntry[]>(mockAuditData);
|
||||
const [filteredData, setFilteredData] = useState<AuditLogEntry[]>(mockAuditData);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filters, setFilters] = useState({
|
||||
dateRange: null as any,
|
||||
action: '',
|
||||
status: '',
|
||||
user: '',
|
||||
resourceType: '',
|
||||
});
|
||||
|
||||
const applyFilters = () => {
|
||||
let filtered = [...auditData];
|
||||
|
||||
if (filters.dateRange && filters.dateRange.length === 2) {
|
||||
const [start, end] = filters.dateRange;
|
||||
filtered = filtered.filter(entry => {
|
||||
const entryDate = dayjs(entry.timestamp);
|
||||
return entryDate.isAfter(start) && entryDate.isBefore(end);
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.action) {
|
||||
filtered = filtered.filter(entry => entry.action === filters.action);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
filtered = filtered.filter(entry => entry.status === filters.status);
|
||||
}
|
||||
|
||||
if (filters.user) {
|
||||
filtered = filtered.filter(entry =>
|
||||
entry.user_id.toLowerCase().includes(filters.user.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.resourceType) {
|
||||
filtered = filtered.filter(entry => entry.resource_type === filters.resourceType);
|
||||
}
|
||||
|
||||
setFilteredData(filtered);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
dateRange: null,
|
||||
action: '',
|
||||
status: '',
|
||||
user: '',
|
||||
resourceType: '',
|
||||
});
|
||||
setFilteredData(auditData);
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
|
||||
case 'failure':
|
||||
return <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />;
|
||||
case 'warning':
|
||||
return <ExclamationCircleOutlined style={{ color: '#faad14' }} />;
|
||||
default:
|
||||
return <ClockCircleOutlined style={{ color: '#1890ff' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getActionIcon = (action: string) => {
|
||||
if (action.includes('APPLICATION')) return <AppstoreOutlined />;
|
||||
if (action.includes('TOKEN')) return <KeyOutlined />;
|
||||
if (action.includes('USER')) return <UserOutlined />;
|
||||
return <AuditOutlined />;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
render: (timestamp: string) => (
|
||||
<div>
|
||||
<div>{dayjs(timestamp).format('MMM DD, YYYY')}</div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{dayjs(timestamp).format('HH:mm:ss')}
|
||||
</Text>
|
||||
</div>
|
||||
),
|
||||
sorter: (a: AuditLogEntry, b: AuditLogEntry) =>
|
||||
dayjs(a.timestamp).unix() - dayjs(b.timestamp).unix(),
|
||||
defaultSortOrder: 'descend' as const,
|
||||
},
|
||||
{
|
||||
title: 'User',
|
||||
dataIndex: 'user_id',
|
||||
key: 'user_id',
|
||||
render: (userId: string) => (
|
||||
<div>
|
||||
<UserOutlined style={{ marginRight: '8px' }} />
|
||||
{userId}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
render: (action: string) => (
|
||||
<div>
|
||||
{getActionIcon(action)}
|
||||
<span style={{ marginLeft: '8px' }}>{action.replace(/_/g, ' ')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Resource',
|
||||
key: 'resource',
|
||||
render: (_: any, record: AuditLogEntry) => (
|
||||
<div>
|
||||
<div>
|
||||
<Tag color="blue">{record.resource_type.toUpperCase()}</Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{record.resource_id}
|
||||
</Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag
|
||||
color={status === 'success' ? 'green' : status === 'failure' ? 'red' : 'orange'}
|
||||
icon={getStatusIcon(status)}
|
||||
>
|
||||
{status.toUpperCase()}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'IP Address',
|
||||
dataIndex: 'ip_address',
|
||||
key: 'ip_address',
|
||||
render: (ip: string) => <Text code>{ip}</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
const expandedRowRender = (record: AuditLogEntry) => (
|
||||
<Card size="small" title="Event Details">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Space direction="vertical" size="small">
|
||||
<div>
|
||||
<Text strong>User Agent:</Text>
|
||||
<div style={{ wordBreak: 'break-all' }}>
|
||||
<Text type="secondary">{record.user_agent}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Event ID:</Text>
|
||||
<div><Text code>{record.id}</Text></div>
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div>
|
||||
<Text strong>Additional Details:</Text>
|
||||
<pre style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
marginTop: '8px'
|
||||
}}>
|
||||
{JSON.stringify(record.details, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Title level={2}>Audit Log</Title>
|
||||
<Text type="secondary">
|
||||
Monitor and track all system activities and security events
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AuditOutlined style={{ fontSize: '32px', color: '#1890ff', marginBottom: '8px' }} />
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>{filteredData.length}</div>
|
||||
<div>Total Events</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<CheckCircleOutlined style={{ fontSize: '32px', color: '#52c41a', marginBottom: '8px' }} />
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
|
||||
{filteredData.filter(e => e.status === 'success').length}
|
||||
</div>
|
||||
<div>Successful</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<ExclamationCircleOutlined style={{ fontSize: '32px', color: '#ff4d4f', marginBottom: '8px' }} />
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
|
||||
{filteredData.filter(e => e.status === 'failure').length}
|
||||
</div>
|
||||
<div>Failed</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<UserOutlined style={{ fontSize: '32px', color: '#722ed1', marginBottom: '8px' }} />
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
|
||||
{new Set(filteredData.map(e => e.user_id)).size}
|
||||
</div>
|
||||
<div>Unique Users</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Filters */}
|
||||
<Card title="Filters" extra={
|
||||
<Space>
|
||||
<Button onClick={applyFilters} type="primary" icon={<SearchOutlined />}>
|
||||
Apply Filters
|
||||
</Button>
|
||||
<Button onClick={clearFilters} icon={<DeleteOutlined />}>
|
||||
Clear
|
||||
</Button>
|
||||
</Space>
|
||||
}>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong>Date Range:</Text>
|
||||
</div>
|
||||
<RangePicker
|
||||
style={{ width: '100%' }}
|
||||
value={filters.dateRange}
|
||||
onChange={(dates) => setFilters({ ...filters, dateRange: dates })}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong>Action:</Text>
|
||||
</div>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="All actions"
|
||||
value={filters.action}
|
||||
onChange={(value) => setFilters({ ...filters, action: value })}
|
||||
allowClear
|
||||
>
|
||||
<Option value="CREATE_APPLICATION">Create Application</Option>
|
||||
<Option value="UPDATE_APPLICATION">Update Application</Option>
|
||||
<Option value="DELETE_APPLICATION">Delete Application</Option>
|
||||
<Option value="CREATE_TOKEN">Create Token</Option>
|
||||
<Option value="DELETE_TOKEN">Delete Token</Option>
|
||||
<Option value="VERIFY_TOKEN">Verify Token</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong>Status:</Text>
|
||||
</div>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="All statuses"
|
||||
value={filters.status}
|
||||
onChange={(value) => setFilters({ ...filters, status: value })}
|
||||
allowClear
|
||||
>
|
||||
<Option value="success">Success</Option>
|
||||
<Option value="failure">Failure</Option>
|
||||
<Option value="warning">Warning</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={5}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong>User:</Text>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search by user"
|
||||
value={filters.user}
|
||||
onChange={(e) => setFilters({ ...filters, user: e.target.value })}
|
||||
allowClear
|
||||
/>
|
||||
</Col>
|
||||
<Col span={5}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong>Resource Type:</Text>
|
||||
</div>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="All types"
|
||||
value={filters.resourceType}
|
||||
onChange={(value) => setFilters({ ...filters, resourceType: value })}
|
||||
allowClear
|
||||
>
|
||||
<Option value="application">Application</Option>
|
||||
<Option value="token">Token</Option>
|
||||
<Option value="user">User</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity Timeline */}
|
||||
<Card title="Recent Activity">
|
||||
<Timeline>
|
||||
{filteredData.slice(0, 5).map((entry) => (
|
||||
<Timeline.Item
|
||||
key={entry.id}
|
||||
dot={getStatusIcon(entry.status)}
|
||||
color={entry.status === 'success' ? 'green' : entry.status === 'failure' ? 'red' : 'orange'}
|
||||
>
|
||||
<div>
|
||||
<Text strong>{entry.action.replace(/_/g, ' ')}</Text>
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
{entry.user_id} • {dayjs(entry.timestamp).fromNow()}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Tag>{entry.resource_type}</Tag>
|
||||
<Text type="secondary" style={{ marginLeft: '8px' }}>
|
||||
{entry.resource_id}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Timeline.Item>
|
||||
))}
|
||||
</Timeline>
|
||||
</Card>
|
||||
|
||||
{/* Audit Log Table */}
|
||||
<Card title="Audit Log Entries">
|
||||
<Alert
|
||||
message="Demo Data"
|
||||
description="This audit log shows simulated data for demonstration purposes. In production, this would display real audit events from your KMS system."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
expandable={{
|
||||
expandedRowRender,
|
||||
expandRowByClick: true,
|
||||
}}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]} of ${total} audit entries`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Audit;
|
||||
Reference in New Issue
Block a user