This commit is contained in:
2025-08-22 19:19:16 -04:00
parent df567983c1
commit 93831d4fad
36 changed files with 22412 additions and 29 deletions

View File

@ -0,0 +1,487 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Space,
Typography,
Modal,
Form,
Input,
Select,
message,
Popconfirm,
Tag,
Card,
Row,
Col,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
CopyOutlined,
} from '@ant-design/icons';
import { apiService, Application, CreateApplicationRequest } from '../services/apiService';
import dayjs from 'dayjs';
const { Title, Text } = Typography;
const { Option } = Select;
const Applications: React.FC = () => {
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [editingApp, setEditingApp] = useState<Application | null>(null);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [selectedApp, setSelectedApp] = useState<Application | null>(null);
const [form] = Form.useForm();
useEffect(() => {
loadApplications();
}, []);
const loadApplications = async () => {
try {
setLoading(true);
const response = await apiService.getApplications();
setApplications(response.data);
} catch (error) {
console.error('Failed to load applications:', error);
message.error('Failed to load applications');
} finally {
setLoading(false);
}
};
const handleCreate = () => {
setEditingApp(null);
form.resetFields();
setModalVisible(true);
};
const handleEdit = (app: Application) => {
setEditingApp(app);
form.setFieldsValue({
app_id: app.app_id,
app_link: app.app_link,
type: app.type,
callback_url: app.callback_url,
token_renewal_duration: formatDuration(app.token_renewal_duration),
max_token_duration: formatDuration(app.max_token_duration),
owner_type: app.owner.type,
owner_name: app.owner.name,
owner_owner: app.owner.owner,
});
setModalVisible(true);
};
const handleDelete = async (appId: string) => {
try {
await apiService.deleteApplication(appId);
message.success('Application deleted successfully');
loadApplications();
} catch (error) {
console.error('Failed to delete application:', error);
message.error('Failed to delete application');
}
};
const handleSubmit = async (values: any) => {
try {
const requestData: CreateApplicationRequest = {
app_id: values.app_id,
app_link: values.app_link,
type: values.type,
callback_url: values.callback_url,
token_renewal_duration: values.token_renewal_duration,
max_token_duration: values.max_token_duration,
owner: {
type: values.owner_type,
name: values.owner_name,
owner: values.owner_owner,
},
};
if (editingApp) {
await apiService.updateApplication(editingApp.app_id, requestData);
message.success('Application updated successfully');
} else {
await apiService.createApplication(requestData);
message.success('Application created successfully');
}
setModalVisible(false);
loadApplications();
} catch (error) {
console.error('Failed to save application:', error);
message.error('Failed to save application');
}
};
const showDetails = (app: Application) => {
setSelectedApp(app);
setDetailModalVisible(true);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.success('Copied to clipboard');
};
const formatDuration = (nanoseconds: number): string => {
const hours = Math.floor(nanoseconds / (1000000000 * 60 * 60));
return `${hours}h`;
};
const columns = [
{
title: 'App ID',
dataIndex: 'app_id',
key: 'app_id',
render: (text: string) => <Text code>{text}</Text>,
},
{
title: 'App Link',
dataIndex: 'app_link',
key: 'app_link',
render: (text: string) => (
<a href={text} target="_blank" rel="noopener noreferrer">
{text}
</a>
),
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
render: (types: string[]) => (
<>
{types.map((type) => (
<Tag key={type} color={type === 'static' ? 'blue' : 'green'}>
{type.toUpperCase()}
</Tag>
))}
</>
),
},
{
title: 'Owner',
dataIndex: 'owner',
key: 'owner',
render: (owner: Application['owner']) => (
<div>
<div>{owner.name}</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{owner.type} {owner.owner}
</Text>
</div>
),
},
{
title: 'Created',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => dayjs(date).format('MMM DD, YYYY'),
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: Application) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => showDetails(record)}
title="View Details"
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
title="Edit"
/>
<Popconfirm
title="Are you sure you want to delete this application?"
onConfirm={() => handleDelete(record.app_id)}
okText="Yes"
cancelText="No"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
title="Delete"
/>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Title level={2}>Applications</Title>
<Text type="secondary">
Manage your applications and their configurations
</Text>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
Create Application
</Button>
</div>
<Table
columns={columns}
dataSource={applications}
rowKey="app_id"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} applications`,
}}
/>
</Space>
{/* Create/Edit Modal */}
<Modal
title={editingApp ? 'Edit Application' : 'Create Application'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={() => form.submit()}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="app_id"
label="Application ID"
rules={[{ required: true, message: 'Please enter application ID' }]}
>
<Input placeholder="com.example.app" disabled={!!editingApp} />
</Form.Item>
<Form.Item
name="app_link"
label="Application Link"
rules={[
{ required: true, message: 'Please enter application link' },
{ type: 'url', message: 'Please enter a valid URL' },
]}
>
<Input placeholder="https://example.com" />
</Form.Item>
<Form.Item
name="type"
label="Application Type"
rules={[{ required: true, message: 'Please select application type' }]}
>
<Select mode="multiple" placeholder="Select types">
<Option value="static">Static</Option>
<Option value="user">User</Option>
</Select>
</Form.Item>
<Form.Item
name="callback_url"
label="Callback URL"
rules={[
{ required: true, message: 'Please enter callback URL' },
{ type: 'url', message: 'Please enter a valid URL' },
]}
>
<Input placeholder="https://example.com/callback" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="token_renewal_duration"
label="Token Renewal Duration"
rules={[{ required: true, message: 'Please enter duration' }]}
>
<Input placeholder="168h" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="max_token_duration"
label="Max Token Duration"
rules={[{ required: true, message: 'Please enter duration' }]}
>
<Input placeholder="720h" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="owner_type"
label="Owner Type"
rules={[{ required: true, message: 'Please select owner type' }]}
>
<Select placeholder="Select owner type">
<Option value="individual">Individual</Option>
<Option value="team">Team</Option>
</Select>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="owner_name"
label="Owner Name"
rules={[{ required: true, message: 'Please enter owner name' }]}
>
<Input placeholder="John Doe" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="owner_owner"
label="Owner Contact"
rules={[{ required: true, message: 'Please enter owner contact' }]}
>
<Input placeholder="john.doe@example.com" />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
{/* Details Modal */}
<Modal
title="Application Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>
Close
</Button>,
]}
width={700}
>
{selectedApp && (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card title="Basic Information">
<Row gutter={16}>
<Col span={12}>
<Text strong>App ID:</Text>
<div>
<Text code>{selectedApp.app_id}</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedApp.app_id)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>App Link:</Text>
<div>
<a href={selectedApp.app_link} target="_blank" rel="noopener noreferrer">
{selectedApp.app_link}
</a>
</div>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Text strong>Type:</Text>
<div>
{selectedApp.type.map((type) => (
<Tag key={type} color={type === 'static' ? 'blue' : 'green'}>
{type.toUpperCase()}
</Tag>
))}
</div>
</Col>
<Col span={12}>
<Text strong>Callback URL:</Text>
<div>{selectedApp.callback_url}</div>
</Col>
</Row>
</Card>
<Card title="Security Configuration">
<Row gutter={16}>
<Col span={12}>
<Text strong>HMAC Key:</Text>
<div>
<Text code></Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedApp.hmac_key)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>Token Renewal Duration:</Text>
<div>{formatDuration(selectedApp.token_renewal_duration)}</div>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={12}>
<Text strong>Max Token Duration:</Text>
<div>{formatDuration(selectedApp.max_token_duration)}</div>
</Col>
</Row>
</Card>
<Card title="Owner Information">
<Row gutter={16}>
<Col span={8}>
<Text strong>Type:</Text>
<div>
<Tag color={selectedApp.owner.type === 'individual' ? 'blue' : 'green'}>
{selectedApp.owner.type.toUpperCase()}
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Name:</Text>
<div>{selectedApp.owner.name}</div>
</Col>
<Col span={8}>
<Text strong>Contact:</Text>
<div>{selectedApp.owner.owner}</div>
</Col>
</Row>
</Card>
<Card title="Timestamps">
<Row gutter={16}>
<Col span={12}>
<Text strong>Created:</Text>
<div>{dayjs(selectedApp.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
<Col span={12}>
<Text strong>Updated:</Text>
<div>{dayjs(selectedApp.updated_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
</Row>
</Card>
</Space>
)}
</Modal>
</div>
);
};
export default Applications;

View 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;

View File

@ -0,0 +1,228 @@
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Statistic, Typography, Space, Alert, Spin } from 'antd';
import {
AppstoreOutlined,
KeyOutlined,
UserOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { apiService } from '../services/apiService';
const { Title } = Typography;
interface DashboardStats {
totalApplications: number;
totalTokens: number;
healthStatus: 'healthy' | 'unhealthy';
readinessStatus: 'ready' | 'not-ready';
}
const Dashboard: React.FC = () => {
const [stats, setStats] = useState<DashboardStats>({
totalApplications: 0,
totalTokens: 0,
healthStatus: 'unhealthy',
readinessStatus: 'not-ready',
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
useEffect(() => {
loadDashboardData();
}, []);
const loadDashboardData = async () => {
try {
setLoading(true);
setError('');
// Load health status
const [healthResponse, readinessResponse] = await Promise.allSettled([
apiService.healthCheck(),
apiService.readinessCheck(),
]);
const healthStatus = healthResponse.status === 'fulfilled' ? 'healthy' : 'unhealthy';
const readinessStatus = readinessResponse.status === 'fulfilled' ? 'ready' : 'not-ready';
// Load applications count
let totalApplications = 0;
let totalTokens = 0;
try {
const appsResponse = await apiService.getApplications(100, 0);
totalApplications = appsResponse.count;
// Count tokens across all applications
for (const app of appsResponse.data) {
try {
const tokensResponse = await apiService.getTokensForApplication(app.app_id, 100, 0);
totalTokens += tokensResponse.count;
} catch (tokenError) {
console.warn(`Failed to load tokens for app ${app.app_id}:`, tokenError);
}
}
} catch (appsError) {
console.warn('Failed to load applications:', appsError);
}
setStats({
totalApplications,
totalTokens,
healthStatus,
readinessStatus,
});
} catch (err) {
console.error('Dashboard error:', err);
setError('Failed to load dashboard data. Please check your connection.');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>Loading dashboard...</div>
</div>
);
}
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={2}>Dashboard</Title>
<p>Welcome to the Key Management System dashboard. Monitor your applications, tokens, and system health.</p>
</div>
{error && (
<Alert
message="Error"
description={error}
type="error"
showIcon
closable
onClose={() => setError('')}
/>
)}
{/* System Status */}
<Card title="System Status" style={{ marginBottom: '24px' }}>
<Row gutter={16}>
<Col span={12}>
<Card>
<Statistic
title="Health Status"
value={stats.healthStatus === 'healthy' ? 'Healthy' : 'Unhealthy'}
prefix={
stats.healthStatus === 'healthy' ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)
}
valueStyle={{
color: stats.healthStatus === 'healthy' ? '#52c41a' : '#ff4d4f',
}}
/>
</Card>
</Col>
<Col span={12}>
<Card>
<Statistic
title="Readiness Status"
value={stats.readinessStatus === 'ready' ? 'Ready' : 'Not Ready'}
prefix={
stats.readinessStatus === 'ready' ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
) : (
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
)
}
valueStyle={{
color: stats.readinessStatus === 'ready' ? '#52c41a' : '#ff4d4f',
}}
/>
</Card>
</Col>
</Row>
</Card>
{/* Statistics */}
<Row gutter={16}>
<Col xs={24} sm={12} lg={8}>
<Card>
<Statistic
title="Total Applications"
value={stats.totalApplications}
prefix={<AppstoreOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={8}>
<Card>
<Statistic
title="Total Tokens"
value={stats.totalTokens}
prefix={<KeyOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={8}>
<Card>
<Statistic
title="Active Users"
value={1}
prefix={<UserOutlined />}
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
</Row>
{/* Quick Actions */}
<Card title="Quick Actions">
<Row gutter={16}>
<Col xs={24} sm={8}>
<Card
hoverable
onClick={() => window.location.pathname = '/applications'}
style={{ textAlign: 'center', cursor: 'pointer' }}
>
<AppstoreOutlined style={{ fontSize: '32px', color: '#1890ff', marginBottom: '8px' }} />
<div>Manage Applications</div>
</Card>
</Col>
<Col xs={24} sm={8}>
<Card
hoverable
onClick={() => window.location.pathname = '/tokens'}
style={{ textAlign: 'center', cursor: 'pointer' }}
>
<KeyOutlined style={{ fontSize: '32px', color: '#52c41a', marginBottom: '8px' }} />
<div>Manage Tokens</div>
</Card>
</Col>
<Col xs={24} sm={8}>
<Card
hoverable
onClick={() => window.location.pathname = '/audit'}
style={{ textAlign: 'center', cursor: 'pointer' }}
>
<ExclamationCircleOutlined style={{ fontSize: '32px', color: '#fa8c16', marginBottom: '8px' }} />
<div>View Audit Log</div>
</Card>
</Col>
</Row>
</Card>
</Space>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,112 @@
import React, { useState } from 'react';
import { Form, Input, Button, Card, Typography, Space, Alert } from 'antd';
import { UserOutlined, KeyOutlined } from '@ant-design/icons';
import { useAuth } from '../contexts/AuthContext';
const { Title, Text } = Typography;
const Login: React.FC = () => {
const [form] = Form.useForm();
const { login, loading } = useAuth();
const [error, setError] = useState<string>('');
const onFinish = async (values: { email: string }) => {
setError('');
const success = await login(values.email);
if (!success) {
setError('Login failed. Please check your email and try again.');
}
};
return (
<div style={{
minHeight: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '20px'
}}>
<Card
style={{
width: '100%',
maxWidth: 400,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
borderRadius: '12px'
}}
>
<Space direction="vertical" size="large" style={{ width: '100%', textAlign: 'center' }}>
<div>
<KeyOutlined style={{ fontSize: '48px', color: '#1890ff', marginBottom: '16px' }} />
<Title level={2} style={{ margin: 0, color: '#262626' }}>
KMS Login
</Title>
<Text type="secondary">
Key Management System
</Text>
</div>
{error && (
<Alert
message={error}
type="error"
showIcon
closable
onClose={() => setError('')}
/>
)}
<Alert
message="Demo Login"
description="Enter any email address to access the demo. In production, this would integrate with your authentication system."
type="info"
showIcon
/>
<Form
form={form}
name="login"
onFinish={onFinish}
layout="vertical"
size="large"
>
<Form.Item
name="email"
label="Email Address"
rules={[
{ required: true, message: 'Please input your email!' },
{ type: 'email', message: 'Please enter a valid email address!' }
]}
>
<Input
prefix={<UserOutlined />}
placeholder="Enter your email"
autoComplete="email"
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Button
type="primary"
htmlType="submit"
loading={loading}
block
style={{ height: '44px', fontSize: '16px' }}
>
{loading ? 'Signing In...' : 'Sign In'}
</Button>
</Form.Item>
</Form>
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
Demo credentials: Use any valid email address
</Text>
</div>
</Space>
</Card>
</div>
);
};
export default Login;

View File

@ -0,0 +1,652 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Space,
Typography,
Modal,
Form,
Input,
Select,
message,
Popconfirm,
Tag,
Card,
Row,
Col,
Alert,
Checkbox,
} from 'antd';
import {
PlusOutlined,
DeleteOutlined,
EyeOutlined,
CopyOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { apiService, Application, StaticToken, CreateTokenRequest, CreateTokenResponse, VerifyRequest } from '../services/apiService';
import dayjs from 'dayjs';
const { Title, Text } = Typography;
const { Option } = Select;
const { TextArea } = Input;
interface TokenWithApp extends StaticToken {
app?: Application;
}
const availablePermissions = [
'app.read',
'app.write',
'app.delete',
'token.read',
'token.create',
'token.revoke',
'repo.read',
'repo.write',
'repo.admin',
'permission.read',
'permission.write',
'permission.grant',
'permission.revoke',
];
const Tokens: React.FC = () => {
const [tokens, setTokens] = useState<TokenWithApp[]>([]);
const [applications, setApplications] = useState<Application[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [verifyModalVisible, setVerifyModalVisible] = useState(false);
const [tokenDetailsVisible, setTokenDetailsVisible] = useState(false);
const [selectedToken, setSelectedToken] = useState<TokenWithApp | null>(null);
const [newTokenResponse, setNewTokenResponse] = useState<CreateTokenResponse | null>(null);
const [form] = Form.useForm();
const [verifyForm] = Form.useForm();
useEffect(() => {
loadApplications();
}, []);
const loadApplications = async () => {
try {
setLoading(true);
const response = await apiService.getApplications();
setApplications(response.data);
if (response.data.length > 0) {
loadAllTokens(response.data);
}
} catch (error) {
console.error('Failed to load applications:', error);
message.error('Failed to load applications');
} finally {
setLoading(false);
}
};
const loadAllTokens = async (apps: Application[]) => {
try {
const allTokens: TokenWithApp[] = [];
for (const app of apps) {
try {
const tokensResponse = await apiService.getTokensForApplication(app.app_id);
const tokensWithApp = tokensResponse.data.map(token => ({
...token,
app,
}));
allTokens.push(...tokensWithApp);
} catch (error) {
console.warn(`Failed to load tokens for app ${app.app_id}:`, error);
}
}
setTokens(allTokens);
} catch (error) {
console.error('Failed to load tokens:', error);
message.error('Failed to load tokens');
}
};
const handleCreate = () => {
form.resetFields();
setNewTokenResponse(null);
setModalVisible(true);
};
const handleDelete = async (tokenId: string) => {
try {
await apiService.deleteToken(tokenId);
message.success('Token deleted successfully');
loadApplications();
} catch (error) {
console.error('Failed to delete token:', error);
message.error('Failed to delete token');
}
};
const handleSubmit = async (values: any) => {
try {
// Debug logging to identify the issue
console.log('Form values:', values);
console.log('App ID:', values.app_id);
if (!values.app_id) {
message.error('Please select an application');
return;
}
const requestData: CreateTokenRequest = {
owner: {
type: values.owner_type,
name: values.owner_name,
owner: values.owner_owner,
},
permissions: values.permissions,
};
console.log('Creating token for app:', values.app_id);
console.log('Request data:', requestData);
const response = await apiService.createToken(values.app_id, requestData);
setNewTokenResponse(response);
message.success('Token created successfully');
loadApplications();
} catch (error) {
console.error('Failed to create token:', error);
message.error('Failed to create token');
}
};
const handleVerifyToken = async (values: any) => {
try {
const verifyRequest: VerifyRequest = {
app_id: values.app_id,
type: 'static',
token: values.token,
permissions: values.permissions || [],
};
const response = await apiService.verifyToken(verifyRequest);
Modal.info({
title: 'Token Verification Result',
width: 600,
content: (
<div>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<Text strong>Status: </Text>
{response.valid ? (
<Tag color="green" icon={<CheckCircleOutlined />}>VALID</Tag>
) : (
<Tag color="red" icon={<ExclamationCircleOutlined />}>INVALID</Tag>
)}
</div>
{response.permissions && response.permissions.length > 0 && (
<div>
<Text strong>Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{response.permissions.map(permission => (
<Tag key={permission} color="blue">{permission}</Tag>
))}
</div>
</div>
)}
{response.permission_results && (
<div>
<Text strong>Permission Check Results:</Text>
<div style={{ marginTop: '8px' }}>
{Object.entries(response.permission_results).map(([permission, granted]) => (
<div key={permission} style={{ marginBottom: '4px' }}>
<Tag color={granted ? 'green' : 'red'}>
{permission}: {granted ? 'GRANTED' : 'DENIED'}
</Tag>
</div>
))}
</div>
</div>
)}
{response.error && (
<Alert
message="Error"
description={response.error}
type="error"
showIcon
/>
)}
</Space>
</div>
),
});
} catch (error) {
console.error('Failed to verify token:', error);
message.error('Failed to verify token');
}
};
const showTokenDetails = (token: TokenWithApp) => {
setSelectedToken(token);
setTokenDetailsVisible(true);
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.success('Copied to clipboard');
};
const columns = [
{
title: 'Token ID',
dataIndex: 'id',
key: 'id',
render: (text: string) => <Text code>{text.substring(0, 8)}...</Text>,
},
{
title: 'Application',
dataIndex: 'app',
key: 'app',
render: (app: Application) => (
<div>
<Text strong>{app?.app_id || 'Unknown'}</Text>
<br />
<Text type="secondary" style={{ fontSize: '12px' }}>
{app?.app_link || ''}
</Text>
</div>
),
},
{
title: 'Owner',
dataIndex: 'owner',
key: 'owner',
render: (owner: StaticToken['owner']) => (
<div>
<div>{owner.name}</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{owner.type} {owner.owner}
</Text>
</div>
),
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
render: (type: string) => (
<Tag color="blue">{type.toUpperCase()}</Tag>
),
},
{
title: 'Created',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => dayjs(date).format('MMM DD, YYYY'),
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: TokenWithApp) => (
<Space>
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => showTokenDetails(record)}
title="View Details"
/>
<Popconfirm
title="Are you sure you want to delete this token?"
onConfirm={() => handleDelete(record.id)}
okText="Yes"
cancelText="No"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
title="Delete"
/>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Title level={2}>Tokens</Title>
<Text type="secondary">
Manage static tokens for your applications
</Text>
</div>
<Space>
<Button
icon={<CheckCircleOutlined />}
onClick={() => setVerifyModalVisible(true)}
>
Verify Token
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
Create Token
</Button>
</Space>
</div>
<Table
columns={columns}
dataSource={tokens}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} tokens`,
}}
/>
</Space>
{/* Create Token Modal */}
<Modal
title="Create Static Token"
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={() => form.submit()}
width={600}
>
{newTokenResponse ? (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Alert
message="Token Created Successfully"
description="Please copy and save this token securely. It will not be shown again."
type="success"
showIcon
/>
<Card title="New Token Details">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<Text strong>Token ID:</Text>
<div>
<Text code>{newTokenResponse.id}</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(newTokenResponse.id)}
/>
</div>
</div>
<div>
<Text strong>Token:</Text>
<div>
<TextArea
value={newTokenResponse.token}
readOnly
rows={3}
style={{ fontFamily: 'monospace' }}
/>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(newTokenResponse.token)}
style={{ marginTop: '8px' }}
>
Copy Token
</Button>
</div>
</div>
<div>
<Text strong>Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{newTokenResponse.permissions.map(permission => (
<Tag key={permission} color="blue">{permission}</Tag>
))}
</div>
</div>
<div>
<Text strong>Created:</Text>
<div>{dayjs(newTokenResponse.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</div>
</Space>
</Card>
</Space>
) : (
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="app_id"
label="Application"
rules={[{ required: true, message: 'Please select an application' }]}
>
<Select placeholder="Select application">
{applications.map(app => (
<Option key={app.app_id} value={app.app_id}>
{app.app_id}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="permissions"
label="Permissions"
rules={[{ required: true, message: 'Please select at least one permission' }]}
>
<Checkbox.Group>
<Row>
{availablePermissions.map(permission => (
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
<Checkbox value={permission}>{permission}</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item
name="owner_type"
label="Owner Type"
rules={[{ required: true, message: 'Please select owner type' }]}
>
<Select placeholder="Select owner type">
<Option value="individual">Individual</Option>
<Option value="team">Team</Option>
</Select>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="owner_name"
label="Owner Name"
rules={[{ required: true, message: 'Please enter owner name' }]}
>
<Input placeholder="John Doe" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="owner_owner"
label="Owner Contact"
rules={[{ required: true, message: 'Please enter owner contact' }]}
>
<Input placeholder="john.doe@example.com" />
</Form.Item>
</Col>
</Row>
</Form>
)}
</Modal>
{/* Verify Token Modal */}
<Modal
title="Verify Token"
open={verifyModalVisible}
onCancel={() => setVerifyModalVisible(false)}
onOk={() => verifyForm.submit()}
width={600}
>
<Form
form={verifyForm}
layout="vertical"
onFinish={handleVerifyToken}
>
<Form.Item
name="app_id"
label="Application"
rules={[{ required: true, message: 'Please select an application' }]}
>
<Select placeholder="Select application">
{applications.map(app => (
<Option key={app.app_id} value={app.app_id}>
{app.app_id}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="token"
label="Token"
rules={[{ required: true, message: 'Please enter the token to verify' }]}
>
<TextArea
placeholder="Enter the token to verify"
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</Form.Item>
<Form.Item
name="permissions"
label="Permissions to Check (Optional)"
>
<Checkbox.Group>
<Row>
{availablePermissions.map(permission => (
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
<Checkbox value={permission}>{permission}</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Form.Item>
</Form>
</Modal>
{/* Token Details Modal */}
<Modal
title="Token Details"
open={tokenDetailsVisible}
onCancel={() => setTokenDetailsVisible(false)}
footer={[
<Button key="close" onClick={() => setTokenDetailsVisible(false)}>
Close
</Button>,
]}
width={600}
>
{selectedToken && (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card title="Token Information">
<Row gutter={16}>
<Col span={12}>
<Text strong>Token ID:</Text>
<div>
<Text code>{selectedToken.id}</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => copyToClipboard(selectedToken.id)}
/>
</div>
</Col>
<Col span={12}>
<Text strong>Type:</Text>
<div>
<Tag color="blue">{selectedToken.type.toUpperCase()}</Tag>
</div>
</Col>
</Row>
</Card>
<Card title="Application">
<Row gutter={16}>
<Col span={12}>
<Text strong>App ID:</Text>
<div>
<Text code>{selectedToken.app?.app_id}</Text>
</div>
</Col>
<Col span={12}>
<Text strong>App Link:</Text>
<div>
<a href={selectedToken.app?.app_link} target="_blank" rel="noopener noreferrer">
{selectedToken.app?.app_link}
</a>
</div>
</Col>
</Row>
</Card>
<Card title="Owner Information">
<Row gutter={16}>
<Col span={8}>
<Text strong>Type:</Text>
<div>
<Tag color={selectedToken.owner.type === 'individual' ? 'blue' : 'green'}>
{selectedToken.owner.type.toUpperCase()}
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Name:</Text>
<div>{selectedToken.owner.name}</div>
</Col>
<Col span={8}>
<Text strong>Contact:</Text>
<div>{selectedToken.owner.owner}</div>
</Col>
</Row>
</Card>
<Card title="Timestamps">
<Row gutter={16}>
<Col span={12}>
<Text strong>Created:</Text>
<div>{dayjs(selectedToken.created_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
<Col span={12}>
<Text strong>Updated:</Text>
<div>{dayjs(selectedToken.updated_at).format('MMMM DD, YYYY HH:mm:ss')}</div>
</Col>
</Row>
</Card>
</Space>
)}
</Modal>
</div>
);
};
export default Tokens;

View File

@ -0,0 +1,416 @@
import React, { useState } from 'react';
import {
Card,
Typography,
Space,
Form,
Input,
Button,
Select,
Alert,
Row,
Col,
Tag,
Modal,
message,
} from 'antd';
import {
UserOutlined,
LoginOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import { apiService } from '../services/apiService';
import { useAuth } from '../contexts/AuthContext';
const { Title, Text } = Typography;
const { Option } = Select;
const Users: React.FC = () => {
const { user } = useAuth();
const [loginForm] = Form.useForm();
const [renewForm] = Form.useForm();
const [loginModalVisible, setLoginModalVisible] = useState(false);
const [renewModalVisible, setRenewModalVisible] = useState(false);
const [loginResult, setLoginResult] = useState<any>(null);
const [renewResult, setRenewResult] = useState<any>(null);
const handleUserLogin = async (values: any) => {
try {
const response = await apiService.login(
values.app_id,
values.permissions || [],
values.redirect_uri
);
setLoginResult(response);
message.success('User login initiated successfully');
} catch (error) {
console.error('Failed to initiate user login:', error);
message.error('Failed to initiate user login');
}
};
const handleTokenRenewal = async (values: any) => {
try {
const response = await apiService.renewToken(
values.app_id,
values.user_id,
values.token
);
setRenewResult(response);
message.success('Token renewed successfully');
} catch (error) {
console.error('Failed to renew token:', error);
message.error('Failed to renew token');
}
};
const availablePermissions = [
'app.read',
'app.write',
'app.delete',
'token.read',
'token.create',
'token.revoke',
'repo.read',
'repo.write',
'repo.admin',
'permission.read',
'permission.write',
'permission.grant',
'permission.revoke',
];
return (
<div>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={2}>User Management</Title>
<Text type="secondary">
Manage user authentication and token operations
</Text>
</div>
{/* Current User Info */}
<Card title="Current User">
<Row gutter={16}>
<Col span={8}>
<Text strong>Email:</Text>
<div>{user?.email}</div>
</Col>
<Col span={8}>
<Text strong>Status:</Text>
<div>
<Tag color="green" icon={<CheckCircleOutlined />}>
AUTHENTICATED
</Tag>
</div>
</Col>
<Col span={8}>
<Text strong>Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{user?.permissions.map(permission => (
<Tag key={permission} color="blue" style={{ marginBottom: '4px' }}>
{permission}
</Tag>
))}
</div>
</Col>
</Row>
</Card>
{/* User Operations */}
<Row gutter={16}>
<Col span={12}>
<Card
title="User Login"
extra={
<Button
type="primary"
icon={<LoginOutlined />}
onClick={() => setLoginModalVisible(true)}
>
Initiate Login
</Button>
}
>
<Text type="secondary">
Initiate a user authentication flow for an application. This will generate
a user token that can be used for API access with specific permissions.
</Text>
</Card>
</Col>
<Col span={12}>
<Card
title="Token Renewal"
extra={
<Button
icon={<ClockCircleOutlined />}
onClick={() => setRenewModalVisible(true)}
>
Renew Token
</Button>
}
>
<Text type="secondary">
Renew an existing user token to extend its validity period.
This is useful for maintaining long-running sessions.
</Text>
</Card>
</Col>
</Row>
{/* Information Cards */}
<Row gutter={16}>
<Col span={8}>
<Card>
<div style={{ textAlign: 'center' }}>
<UserOutlined style={{ fontSize: '32px', color: '#1890ff', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>1</div>
<div>Active Users</div>
</div>
</Card>
</Col>
<Col span={8}>
<Card>
<div style={{ textAlign: 'center' }}>
<LoginOutlined style={{ fontSize: '32px', color: '#52c41a', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>Demo</div>
<div>Authentication Mode</div>
</div>
</Card>
</Col>
<Col span={8}>
<Card>
<div style={{ textAlign: 'center' }}>
<CheckCircleOutlined style={{ fontSize: '32px', color: '#722ed1', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>Active</div>
<div>Session Status</div>
</div>
</Card>
</Col>
</Row>
{/* Authentication Flow Information */}
<Card title="Authentication Flow Information">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Alert
message="Demo Mode"
description="This frontend is running in demo mode. In a production environment, user authentication would integrate with your identity provider (OAuth2, SAML, etc.)."
type="info"
showIcon
/>
<div>
<Title level={4}>Supported Authentication Methods</Title>
<ul>
<li><strong>Header Authentication:</strong> Uses X-User-Email header for demo purposes</li>
<li><strong>OAuth2:</strong> Standard OAuth2 flow with authorization code grant</li>
<li><strong>SAML:</strong> SAML 2.0 single sign-on integration</li>
<li><strong>JWT:</strong> JSON Web Token based authentication</li>
</ul>
</div>
<div>
<Title level={4}>Token Types</Title>
<ul>
<li><strong>User Tokens:</strong> Short-lived tokens for authenticated users</li>
<li><strong>Static Tokens:</strong> Long-lived tokens for service-to-service communication</li>
<li><strong>Renewal Tokens:</strong> Used to extend user token validity</li>
</ul>
</div>
</Space>
</Card>
</Space>
{/* User Login Modal */}
<Modal
title="Initiate User Login"
open={loginModalVisible}
onCancel={() => {
setLoginModalVisible(false);
setLoginResult(null);
}}
onOk={() => loginForm.submit()}
width={600}
>
{loginResult ? (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Alert
message="Login Flow Initiated"
description="The user login flow has been initiated successfully."
type="success"
showIcon
/>
<Card title="Login Response">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{loginResult.redirect_url && (
<div>
<Text strong>Redirect URL:</Text>
<div>
<a href={loginResult.redirect_url} target="_blank" rel="noopener noreferrer">
{loginResult.redirect_url}
</a>
</div>
</div>
)}
{loginResult.token && (
<div>
<Text strong>Token:</Text>
<div>
<Input.TextArea
value={loginResult.token}
readOnly
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</div>
</div>
)}
{loginResult.user_id && (
<div>
<Text strong>User ID:</Text>
<div>{loginResult.user_id}</div>
</div>
)}
{loginResult.expires_in && (
<div>
<Text strong>Expires In:</Text>
<div>{loginResult.expires_in} seconds</div>
</div>
)}
</Space>
</Card>
</Space>
) : (
<Form
form={loginForm}
layout="vertical"
onFinish={handleUserLogin}
>
<Form.Item
name="app_id"
label="Application ID"
rules={[{ required: true, message: 'Please enter application ID' }]}
>
<Input placeholder="com.example.app" />
</Form.Item>
<Form.Item
name="permissions"
label="Requested Permissions"
>
<Select
mode="multiple"
placeholder="Select permissions"
allowClear
>
{availablePermissions.map(permission => (
<Option key={permission} value={permission}>
{permission}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="redirect_uri"
label="Redirect URI (Optional)"
>
<Input placeholder="https://example.com/callback" />
</Form.Item>
</Form>
)}
</Modal>
{/* Token Renewal Modal */}
<Modal
title="Renew User Token"
open={renewModalVisible}
onCancel={() => {
setRenewModalVisible(false);
setRenewResult(null);
}}
onOk={() => renewForm.submit()}
width={600}
>
{renewResult ? (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Alert
message="Token Renewed Successfully"
description="The user token has been renewed with extended validity."
type="success"
showIcon
/>
<Card title="Renewal Response">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<Text strong>New Token:</Text>
<div>
<Input.TextArea
value={renewResult.token}
readOnly
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</div>
</div>
<div>
<Text strong>Expires At:</Text>
<div>{new Date(renewResult.expires_at).toLocaleString()}</div>
</div>
<div>
<Text strong>Max Valid At:</Text>
<div>{new Date(renewResult.max_valid_at).toLocaleString()}</div>
</div>
</Space>
</Card>
</Space>
) : (
<Form
form={renewForm}
layout="vertical"
onFinish={handleTokenRenewal}
>
<Form.Item
name="app_id"
label="Application ID"
rules={[{ required: true, message: 'Please enter application ID' }]}
>
<Input placeholder="com.example.app" />
</Form.Item>
<Form.Item
name="user_id"
label="User ID"
rules={[{ required: true, message: 'Please enter user ID' }]}
>
<Input placeholder="user@example.com" />
</Form.Item>
<Form.Item
name="token"
label="Current Token"
rules={[{ required: true, message: 'Please enter current token' }]}
>
<Input.TextArea
placeholder="Enter the current user token"
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</Form.Item>
</Form>
)}
</Modal>
</div>
);
};
export default Users;