org
This commit is contained in:
532
kms/kms-frontend/src/components/Applications.tsx
Normal file
532
kms/kms-frontend/src/components/Applications.tsx
Normal file
@ -0,0 +1,532 @@
|
||||
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_prefix: app.token_prefix,
|
||||
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_prefix: values.token_prefix,
|
||||
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: 'Token Prefix',
|
||||
dataIndex: 'token_prefix',
|
||||
key: 'token_prefix',
|
||||
render: (prefix: string) => prefix ? <Text code>{prefix}</Text> : <Text type="secondary">Default</Text>,
|
||||
},
|
||||
{
|
||||
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>
|
||||
|
||||
<Form.Item
|
||||
name="token_prefix"
|
||||
label="Token Prefix"
|
||||
rules={[
|
||||
{
|
||||
pattern: /^[A-Z]{2,4}$/,
|
||||
message: 'Token prefix must be 2-4 uppercase letters (e.g., NC for Nerd Completion)'
|
||||
},
|
||||
]}
|
||||
help="Optional custom prefix for tokens. Leave empty for default 'kms_' prefix. Examples: NC → NCT- (static), NCUT- (user)"
|
||||
>
|
||||
<Input
|
||||
placeholder="NC"
|
||||
maxLength={4}
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.toUpperCase();
|
||||
form.setFieldValue('token_prefix', value);
|
||||
}}
|
||||
/>
|
||||
</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 Prefix:</Text>
|
||||
<div>
|
||||
{selectedApp.token_prefix ? (
|
||||
<>
|
||||
<Text code>{selectedApp.token_prefix}</Text>
|
||||
<Text type="secondary" style={{ marginLeft: 8 }}>
|
||||
(Static: {selectedApp.token_prefix}T-, User: {selectedApp.token_prefix}UT-)
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text type="secondary">Default (kms_)</Text>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16} style={{ marginTop: '16px' }}>
|
||||
<Col span={12}>
|
||||
<Text strong>Token Renewal Duration:</Text>
|
||||
<div>{formatDuration(selectedApp.token_renewal_duration)}</div>
|
||||
</Col>
|
||||
<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;
|
||||
465
kms/kms-frontend/src/components/Audit.tsx
Normal file
465
kms/kms-frontend/src/components/Audit.tsx
Normal file
@ -0,0 +1,465 @@
|
||||
import React, { useState, useEffect } 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';
|
||||
import { apiService, AuditEvent, AuditQueryParams } from '../services/apiService';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
|
||||
const Audit: React.FC = () => {
|
||||
const [auditData, setAuditData] = useState<AuditEvent[]>([]);
|
||||
const [filteredData, setFilteredData] = useState<AuditEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState({
|
||||
dateRange: null as any,
|
||||
action: '',
|
||||
status: '',
|
||||
user: '',
|
||||
resourceType: '',
|
||||
});
|
||||
|
||||
// Load audit data on component mount
|
||||
useEffect(() => {
|
||||
loadAuditData();
|
||||
}, []);
|
||||
|
||||
const loadAuditData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiService.getAuditEvents({
|
||||
limit: 100,
|
||||
order_by: 'timestamp',
|
||||
order_desc: true,
|
||||
});
|
||||
setAuditData(response.events);
|
||||
setFilteredData(response.events);
|
||||
} catch (error) {
|
||||
console.error('Failed to load audit data:', error);
|
||||
// Keep empty arrays on error
|
||||
setAuditData([]);
|
||||
setFilteredData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = async () => {
|
||||
// For real-time filtering, we'll use the API with filters
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: AuditQueryParams = {
|
||||
limit: 100,
|
||||
order_by: 'timestamp',
|
||||
order_desc: true,
|
||||
};
|
||||
|
||||
if (filters.dateRange && filters.dateRange.length === 2) {
|
||||
const [start, end] = filters.dateRange;
|
||||
params.start_time = start.toISOString();
|
||||
params.end_time = end.toISOString();
|
||||
}
|
||||
|
||||
if (filters.action) {
|
||||
params.event_types = [filters.action];
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
params.statuses = [filters.status];
|
||||
}
|
||||
|
||||
if (filters.user) {
|
||||
params.actor_id = filters.user;
|
||||
}
|
||||
|
||||
if (filters.resourceType) {
|
||||
params.resource_type = filters.resourceType;
|
||||
}
|
||||
|
||||
const response = await apiService.getAuditEvents(params);
|
||||
setFilteredData(response.events);
|
||||
} catch (error) {
|
||||
console.error('Failed to apply filters:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
dateRange: null,
|
||||
action: '',
|
||||
status: '',
|
||||
user: '',
|
||||
resourceType: '',
|
||||
});
|
||||
loadAuditData(); // Reload original data
|
||||
};
|
||||
|
||||
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: AuditEvent, b: AuditEvent) =>
|
||||
dayjs(a.timestamp).unix() - dayjs(b.timestamp).unix(),
|
||||
defaultSortOrder: 'descend' as const,
|
||||
},
|
||||
{
|
||||
title: 'User',
|
||||
dataIndex: 'actor_id',
|
||||
key: 'actor_id',
|
||||
render: (actorId: string) => (
|
||||
<div>
|
||||
<UserOutlined style={{ marginRight: '8px' }} />
|
||||
{actorId || 'System'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type: string) => (
|
||||
<div>
|
||||
{getActionIcon(type)}
|
||||
<span style={{ marginLeft: '8px' }}>{type.replace(/_/g, ' ').replace(/\./g, ' ')}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Resource',
|
||||
key: 'resource',
|
||||
render: (_: any, record: AuditEvent) => (
|
||||
<div>
|
||||
<div>
|
||||
<Tag color="blue">{record.resource_type?.toUpperCase() || 'N/A'}</Tag>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{record.resource_id || 'N/A'}
|
||||
</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: 'actor_ip',
|
||||
key: 'actor_ip',
|
||||
render: (ip: string) => <Text code>{ip || 'N/A'}</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
const expandedRowRender = (record: AuditEvent) => (
|
||||
<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.actor_id).filter(id => 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="app.created">Application Created</Option>
|
||||
<Option value="app.updated">Application Updated</Option>
|
||||
<Option value="app.deleted">Application Deleted</Option>
|
||||
<Option value="auth.token_created">Token Created</Option>
|
||||
<Option value="auth.token_revoked">Token Revoked</Option>
|
||||
<Option value="auth.token_validated">Token Validated</Option>
|
||||
<Option value="auth.login">Login</Option>
|
||||
<Option value="auth.login_failed">Login Failed</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.type.replace(/_/g, ' ').replace(/\./g, ' ')}</Text>
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
{entry.actor_id || 'System'} • {dayjs(entry.timestamp).fromNow()}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Tag>{entry.resource_type || 'N/A'}</Tag>
|
||||
<Text type="secondary" style={{ marginLeft: '8px' }}>
|
||||
{entry.resource_id || 'N/A'}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Timeline.Item>
|
||||
))}
|
||||
</Timeline>
|
||||
</Card>
|
||||
|
||||
{/* Audit Log Table */}
|
||||
<Card title="Audit Log Entries">
|
||||
{filteredData.length === 0 && !loading && (
|
||||
<Alert
|
||||
message="No Audit Events Found"
|
||||
description="No audit events match your current filters. Try adjusting the filters or check if any events have been logged to the 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;
|
||||
228
kms/kms-frontend/src/components/Dashboard.tsx
Normal file
228
kms/kms-frontend/src/components/Dashboard.tsx
Normal 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;
|
||||
112
kms/kms-frontend/src/components/Login.tsx
Normal file
112
kms/kms-frontend/src/components/Login.tsx
Normal 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;
|
||||
719
kms/kms-frontend/src/components/TokenTester.tsx
Normal file
719
kms/kms-frontend/src/components/TokenTester.tsx
Normal file
@ -0,0 +1,719 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
Alert,
|
||||
Divider,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
Checkbox,
|
||||
message,
|
||||
Modal,
|
||||
Steps,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CopyOutlined,
|
||||
ReloadOutlined,
|
||||
LinkOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { apiService, Application } from '../services/apiService';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
const { Step } = Steps;
|
||||
|
||||
interface LoginTestResult {
|
||||
success: boolean;
|
||||
token?: string;
|
||||
redirectUrl?: string;
|
||||
userId?: string;
|
||||
appId?: string;
|
||||
expiresIn?: number;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface CallbackTestResult {
|
||||
success: boolean;
|
||||
token?: string;
|
||||
verified?: boolean;
|
||||
permitted?: boolean;
|
||||
user_id?: string;
|
||||
permissions?: string[];
|
||||
permission_results?: Record<string, boolean>;
|
||||
expires_at?: string;
|
||||
max_valid_at?: string;
|
||||
token_type?: string;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
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 TokenTester: React.FC = () => {
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
const [callbackLoading, setCallbackLoading] = useState(false);
|
||||
const [loginResult, setLoginResult] = useState<LoginTestResult | null>(null);
|
||||
const [callbackResult, setCallbackResult] = useState<CallbackTestResult | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [callbackModalVisible, setCallbackModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [callbackForm] = Form.useForm();
|
||||
const [useCallback, setUseCallback] = useState(false);
|
||||
const [extractedToken, setExtractedToken] = useState('');
|
||||
|
||||
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 handleLoginTest = async (values: any) => {
|
||||
try {
|
||||
setTestLoading(true);
|
||||
setCurrentStep(1);
|
||||
|
||||
const selectedApp = applications.find(app => app.app_id === values.app_id);
|
||||
const callbackUrl = `${window.location.origin}/token-tester/callback`;
|
||||
|
||||
// Store test data in localStorage for the callback page
|
||||
const testData = {
|
||||
app_id: values.app_id,
|
||||
permissions: values.permissions || [],
|
||||
use_callback: values.use_callback,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem('token_tester_data', JSON.stringify(testData));
|
||||
|
||||
console.log('Testing login flow with:', {
|
||||
app_id: values.app_id,
|
||||
permissions: values.permissions || [],
|
||||
redirect_uri: values.use_callback ? callbackUrl : undefined,
|
||||
});
|
||||
|
||||
const response = await apiService.login(
|
||||
values.app_id,
|
||||
values.permissions || [],
|
||||
values.use_callback ? callbackUrl : undefined,
|
||||
values.token_delivery || 'query'
|
||||
);
|
||||
|
||||
console.log('Login response:', response);
|
||||
|
||||
const result: LoginTestResult = {
|
||||
success: true,
|
||||
token: response.token,
|
||||
redirectUrl: response.redirect_url,
|
||||
userId: response.user_id,
|
||||
appId: values.app_id,
|
||||
expiresIn: response.expires_in,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setLoginResult(result);
|
||||
setCurrentStep(2);
|
||||
|
||||
message.success('Login test completed successfully!');
|
||||
|
||||
// If we have a redirect URL, show the callback modal
|
||||
if (response.redirect_url && values.use_callback) {
|
||||
setCallbackModalVisible(true);
|
||||
|
||||
// Extract token from redirect URL if using query parameter delivery
|
||||
let tokenFromUrl = '';
|
||||
if (values.token_delivery === 'query') {
|
||||
try {
|
||||
const url = new URL(response.redirect_url);
|
||||
tokenFromUrl = url.searchParams.get('token') || '';
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse redirect URL for token extraction:', e);
|
||||
}
|
||||
}
|
||||
|
||||
setExtractedToken(tokenFromUrl);
|
||||
|
||||
callbackForm.setFieldsValue({
|
||||
app_id: values.app_id,
|
||||
token: tokenFromUrl, // Pre-fill with extracted token if available
|
||||
permissions: values.permissions || [],
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Login test failed:', error);
|
||||
|
||||
const result: LoginTestResult = {
|
||||
success: false,
|
||||
error: error.response?.data?.message || error.message || 'Login test failed',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setLoginResult(result);
|
||||
setCurrentStep(2);
|
||||
message.error('Login test failed');
|
||||
} finally {
|
||||
setTestLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCallbackTest = async (values: any) => {
|
||||
try {
|
||||
setCallbackLoading(true);
|
||||
setCurrentStep(3);
|
||||
|
||||
console.log('Testing callback with token verification:', values);
|
||||
|
||||
// Verify the token received in the callback (type will be auto-detected)
|
||||
const verifyResponse = await apiService.verifyToken({
|
||||
app_id: values.app_id,
|
||||
token: values.token,
|
||||
permissions: values.permissions || [],
|
||||
});
|
||||
|
||||
console.log('Token verification response:', verifyResponse);
|
||||
|
||||
const result: CallbackTestResult = {
|
||||
success: verifyResponse.valid,
|
||||
token: values.token,
|
||||
verified: verifyResponse.valid,
|
||||
permitted: verifyResponse.permitted,
|
||||
user_id: verifyResponse.user_id,
|
||||
permissions: verifyResponse.permissions,
|
||||
permission_results: verifyResponse.permission_results,
|
||||
expires_at: verifyResponse.expires_at,
|
||||
max_valid_at: verifyResponse.max_valid_at,
|
||||
token_type: verifyResponse.token_type,
|
||||
error: verifyResponse.error,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setCallbackResult(result);
|
||||
setCurrentStep(4);
|
||||
|
||||
if (verifyResponse.valid) {
|
||||
message.success('Callback test completed successfully!');
|
||||
// Auto-close modal after successful verification to show results
|
||||
setTimeout(() => {
|
||||
setCallbackModalVisible(false);
|
||||
}, 1500);
|
||||
} else {
|
||||
message.warning('Callback test completed - token verification failed');
|
||||
// Auto-close modal after failed verification to show results
|
||||
setTimeout(() => {
|
||||
setCallbackModalVisible(false);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Callback test failed:', error);
|
||||
|
||||
const result: CallbackTestResult = {
|
||||
success: false,
|
||||
error: error.response?.data?.message || error.message || 'Callback test failed',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setCallbackResult(result);
|
||||
setCurrentStep(4);
|
||||
message.error('Callback test failed');
|
||||
|
||||
// Auto-close modal to show error results
|
||||
setTimeout(() => {
|
||||
setCallbackModalVisible(false);
|
||||
}, 1500);
|
||||
} finally {
|
||||
setCallbackLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetTest = () => {
|
||||
setCurrentStep(0);
|
||||
setLoginResult(null);
|
||||
setCallbackResult(null);
|
||||
setCallbackModalVisible(false);
|
||||
setUseCallback(false);
|
||||
setExtractedToken('');
|
||||
form.resetFields();
|
||||
callbackForm.resetFields();
|
||||
// Clear stored test data
|
||||
localStorage.removeItem('token_tester_data');
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
message.success('Copied to clipboard');
|
||||
};
|
||||
|
||||
const openCallbackUrl = () => {
|
||||
if (loginResult?.redirectUrl) {
|
||||
window.open(loginResult.redirectUrl, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Title level={2}>Token Tester</Title>
|
||||
<Text type="secondary">
|
||||
Test the /login flow and callback handling for user tokens
|
||||
</Text>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Alert
|
||||
message="Two Testing Modes Available"
|
||||
description={
|
||||
<div>
|
||||
<Text strong>Direct Mode:</Text> Login returns token directly in response body (no callback)<br/>
|
||||
<Text strong>Callback Mode:</Text> Login returns redirect URL, token in query parameter (default) or secure cookie
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={resetTest}
|
||||
disabled={testLoading || callbackLoading}
|
||||
>
|
||||
Reset Test
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Test Progress */}
|
||||
<Card title="Test Progress">
|
||||
<Steps current={currentStep} size="small">
|
||||
<Step title="Configure" description="Set up test parameters" />
|
||||
<Step title="Login Test" description="Test /login endpoint" />
|
||||
<Step title="Results" description="Review login results" />
|
||||
<Step title="Callback Test" description="Test callback handling" />
|
||||
<Step title="Complete" description="Test completed" />
|
||||
</Steps>
|
||||
</Card>
|
||||
|
||||
{/* Test Configuration */}
|
||||
<Card title="Test Configuration">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleLoginTest}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="app_id"
|
||||
label="Application"
|
||||
rules={[{ required: true, message: 'Please select an application' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Select application to test"
|
||||
loading={loading}
|
||||
>
|
||||
{applications.map(app => (
|
||||
<Option key={app.app_id} value={app.app_id}>
|
||||
<div>
|
||||
<Text strong>{app.app_id}</Text>
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{app.app_link}
|
||||
</Text>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="use_callback"
|
||||
valuePropName="checked"
|
||||
label=" "
|
||||
>
|
||||
<Checkbox onChange={(e) => setUseCallback(e.target.checked)}>
|
||||
Use callback URL (test full flow)
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="token_delivery"
|
||||
label="Token Delivery Method (for callback flows)"
|
||||
tooltip="Choose how tokens are delivered when using callback URLs"
|
||||
initialValue="query"
|
||||
>
|
||||
<Select placeholder="Select delivery method" disabled={!useCallback} defaultValue="query">
|
||||
<Option value="query">
|
||||
<div>
|
||||
<Text strong>Query Parameter</Text> (Recommended for testing)
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
Token included in callback URL query string
|
||||
</Text>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="cookie">
|
||||
<div>
|
||||
<Text strong>Cookie</Text> (More secure for production)
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
Token stored in HTTP-only cookie
|
||||
</Text>
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="permissions"
|
||||
label="Permissions to Request"
|
||||
>
|
||||
<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>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={testLoading}
|
||||
size="large"
|
||||
>
|
||||
Start Login Test
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* Login Test Results */}
|
||||
{loginResult && (
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
{loginResult.success ? (
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
) : (
|
||||
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
)}
|
||||
Login Test Results
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Alert
|
||||
message={loginResult.success ? 'Login Test Successful' : 'Login Test Failed'}
|
||||
description={loginResult.success
|
||||
? 'The /login endpoint responded successfully'
|
||||
: loginResult.error
|
||||
}
|
||||
type={loginResult.success ? 'success' : 'error'}
|
||||
showIcon
|
||||
/>
|
||||
|
||||
{loginResult.success && (
|
||||
<div>
|
||||
<Row gutter={16}>
|
||||
{loginResult.token && (
|
||||
<Col span={12}>
|
||||
<Card size="small" title="User Token">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<TextArea
|
||||
value={loginResult.token}
|
||||
readOnly
|
||||
rows={3}
|
||||
style={{ fontFamily: 'monospace', fontSize: '12px' }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyToClipboard(loginResult.token!)}
|
||||
>
|
||||
Copy Token
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{loginResult.redirectUrl && (
|
||||
<Col span={12}>
|
||||
<Card size="small" title="Redirect URL">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text code style={{ fontSize: '12px', wordBreak: 'break-all' }}>
|
||||
{loginResult.redirectUrl}
|
||||
</Text>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyToClipboard(loginResult.redirectUrl!)}
|
||||
>
|
||||
Copy URL
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<LinkOutlined />}
|
||||
onClick={openCallbackUrl}
|
||||
>
|
||||
Open URL
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Text strong>User ID:</Text>
|
||||
<div>{loginResult.userId || 'N/A'}</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Text strong>App ID:</Text>
|
||||
<div>{loginResult.appId}</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Text strong>Expires In:</Text>
|
||||
<div>{loginResult.expiresIn ? `${loginResult.expiresIn}s` : 'N/A'}</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Text strong>Timestamp:</Text>
|
||||
<div>{new Date(loginResult.timestamp).toLocaleString()}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Callback Test Results */}
|
||||
{callbackResult && (
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
{callbackResult.success ? (
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
) : (
|
||||
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
)}
|
||||
Callback Test Results
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Alert
|
||||
message={callbackResult.success ? 'Callback Test Successful' : 'Callback Test Failed'}
|
||||
description={callbackResult.success
|
||||
? 'Token verification in callback was successful'
|
||||
: callbackResult.error
|
||||
}
|
||||
type={callbackResult.success ? 'success' : 'error'}
|
||||
showIcon
|
||||
/>
|
||||
|
||||
{callbackResult.success && (
|
||||
<div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card size="small" title="Token Information">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text strong>Token Type:</Text>
|
||||
<div>
|
||||
<Tag color="blue">{(callbackResult.token_type || 'user').toUpperCase()}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{callbackResult.user_id && (
|
||||
<div>
|
||||
<Text strong>User ID:</Text>
|
||||
<div>{callbackResult.user_id}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{callbackResult.expires_at && (
|
||||
<div>
|
||||
<Text strong>Expires At:</Text>
|
||||
<div>{new Date(callbackResult.expires_at).toLocaleString()}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{callbackResult.max_valid_at && (
|
||||
<div>
|
||||
<Text strong>Max Valid Until:</Text>
|
||||
<div>{new Date(callbackResult.max_valid_at).toLocaleString()}</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card size="small" title="Permissions">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{callbackResult.permissions && callbackResult.permissions.length > 0 ? (
|
||||
<div>
|
||||
<Text strong>Available Permissions:</Text>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
{callbackResult.permissions.map(permission => (
|
||||
<Tag key={permission} color="green" style={{ margin: '2px' }}>
|
||||
{permission}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary">No permissions available</Text>
|
||||
)}
|
||||
|
||||
{callbackResult.permission_results && Object.keys(callbackResult.permission_results).length > 0 && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Text strong>Permission Check Results:</Text>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
{Object.entries(callbackResult.permission_results).map(([permission, granted]) => (
|
||||
<div key={permission} style={{ marginBottom: '4px' }}>
|
||||
<Tag color={granted ? 'green' : 'red'}>
|
||||
{permission}: {granted ? 'GRANTED' : 'DENIED'}
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Text strong>Timestamp:</Text>
|
||||
<div>{new Date(callbackResult.timestamp).toLocaleString()}</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{/* Callback Test Modal */}
|
||||
<Modal
|
||||
title="Test Callback Handling"
|
||||
open={callbackModalVisible}
|
||||
onCancel={() => setCallbackModalVisible(false)}
|
||||
onOk={() => callbackForm.submit()}
|
||||
confirmLoading={callbackLoading}
|
||||
width={700}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Alert
|
||||
message="Callback URL Received"
|
||||
description={extractedToken
|
||||
? "Token successfully extracted from callback URL. Verify the token to complete the flow test."
|
||||
: "Redirect URL received. If using cookie delivery, the token is stored in a secure cookie."
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
|
||||
<Form
|
||||
form={callbackForm}
|
||||
layout="vertical"
|
||||
onFinish={handleCallbackTest}
|
||||
>
|
||||
<Form.Item
|
||||
name="app_id"
|
||||
label="Application ID"
|
||||
>
|
||||
<Input readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="token"
|
||||
label="Token from Callback"
|
||||
rules={[{ required: true, message: 'Token is required' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={3}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
placeholder="Token extracted from callback URL"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="permissions"
|
||||
label="Permissions to Verify"
|
||||
>
|
||||
<Checkbox.Group disabled>
|
||||
<Row>
|
||||
{availablePermissions.map(permission => (
|
||||
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
|
||||
<Checkbox value={permission}>{permission}</Checkbox>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenTester;
|
||||
396
kms/kms-frontend/src/components/TokenTesterCallback.tsx
Normal file
396
kms/kms-frontend/src/components/TokenTesterCallback.tsx
Normal file
@ -0,0 +1,396 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Alert,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
Spin,
|
||||
Result,
|
||||
Input,
|
||||
} from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
CopyOutlined,
|
||||
ArrowLeftOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { apiService } from '../services/apiService';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface CallbackData {
|
||||
token?: string;
|
||||
state?: string;
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
}
|
||||
|
||||
interface VerificationResult {
|
||||
valid: boolean;
|
||||
permitted: boolean;
|
||||
user_id?: string;
|
||||
permissions: string[];
|
||||
permission_results?: Record<string, boolean>;
|
||||
expires_at?: string;
|
||||
max_valid_at?: string;
|
||||
token_type: string;
|
||||
claims?: Record<string, string>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const TokenTesterCallback: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [callbackData, setCallbackData] = useState<CallbackData>({});
|
||||
const [verificationResult, setVerificationResult] = useState<VerificationResult | null>(null);
|
||||
const [verificationError, setVerificationError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
parseCallbackData();
|
||||
}, [location]);
|
||||
|
||||
const parseCallbackData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Parse URL parameters
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
let token = urlParams.get('token') || undefined;
|
||||
|
||||
// If no token in URL, try to extract from auth_token cookie
|
||||
if (!token) {
|
||||
token = getCookie('auth_token') || undefined;
|
||||
}
|
||||
|
||||
const data: CallbackData = {
|
||||
token: token,
|
||||
state: urlParams.get('state') || undefined,
|
||||
error: urlParams.get('error') || undefined,
|
||||
error_description: urlParams.get('error_description') || undefined,
|
||||
};
|
||||
|
||||
setCallbackData(data);
|
||||
|
||||
// If we have a token, try to verify it
|
||||
if (data.token && !data.error) {
|
||||
await verifyToken(data.token);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error parsing callback data:', error);
|
||||
setVerificationError('Failed to parse callback data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function to get cookie value by name
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) {
|
||||
const cookieValue = parts.pop()?.split(';').shift();
|
||||
return cookieValue || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const verifyToken = async (token: string) => {
|
||||
try {
|
||||
// We need to extract app_id from the state or make a best guess
|
||||
// For now, we'll try to verify without specifying app_id
|
||||
// In a real implementation, the app_id should be included in the state parameter
|
||||
|
||||
// Try to get app_id from localStorage if it was stored during the test
|
||||
const testData = localStorage.getItem('token_tester_data');
|
||||
let appId = '';
|
||||
|
||||
if (testData) {
|
||||
try {
|
||||
const parsed = JSON.parse(testData);
|
||||
appId = parsed.app_id || '';
|
||||
} catch (e) {
|
||||
console.warn('Could not parse stored test data');
|
||||
}
|
||||
}
|
||||
|
||||
if (!appId) {
|
||||
// If we don't have app_id, we can't verify the token properly
|
||||
setVerificationError('Cannot verify token: Application ID not found in callback state');
|
||||
return;
|
||||
}
|
||||
|
||||
const verifyRequest = {
|
||||
app_id: appId,
|
||||
token: token,
|
||||
permissions: [], // We'll verify without specific permissions
|
||||
};
|
||||
|
||||
const result = await apiService.verifyToken(verifyRequest);
|
||||
setVerificationResult(result);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Token verification failed:', error);
|
||||
setVerificationError(
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
'Token verification failed'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
const goBackToTester = () => {
|
||||
navigate('/token-tester');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '400px'
|
||||
}}>
|
||||
<Spin size="large" />
|
||||
<Text style={{ marginLeft: '16px' }}>Processing callback...</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Title level={2}>Token Tester - Callback</Title>
|
||||
<Text type="secondary">
|
||||
Callback page for testing the login flow
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={goBackToTester}
|
||||
>
|
||||
Back to Tester
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Callback Status */}
|
||||
<Card title="Callback Status">
|
||||
{callbackData.error ? (
|
||||
<Alert
|
||||
message="Callback Error"
|
||||
description={`${callbackData.error}: ${callbackData.error_description || 'No description provided'}`}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
) : callbackData.token ? (
|
||||
<Alert
|
||||
message="Callback Successful"
|
||||
description="Token received successfully from the login flow"
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
message="Invalid Callback"
|
||||
description="No token or error information found in callback URL"
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Token Information */}
|
||||
{callbackData.token && (
|
||||
<Card title="Received Token">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text strong>Token:</Text>
|
||||
<TextArea
|
||||
value={callbackData.token}
|
||||
readOnly
|
||||
rows={4}
|
||||
style={{ fontFamily: 'monospace', fontSize: '12px', marginTop: '8px' }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyToClipboard(callbackData.token!)}
|
||||
style={{ marginTop: '8px' }}
|
||||
>
|
||||
Copy Token
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{callbackData.state && (
|
||||
<div>
|
||||
<Text strong>State:</Text>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<Text code>{callbackData.state}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Token Verification Results */}
|
||||
{verificationResult && (
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
{verificationResult.valid ? (
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
) : (
|
||||
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
)}
|
||||
Token Verification Results
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Alert
|
||||
message={verificationResult.valid ? 'Token Valid' : 'Token Invalid'}
|
||||
description={verificationResult.valid
|
||||
? 'The token was successfully verified'
|
||||
: verificationResult.error || 'Token verification failed'
|
||||
}
|
||||
type={verificationResult.valid ? 'success' : 'error'}
|
||||
showIcon
|
||||
/>
|
||||
|
||||
{verificationResult.valid && (
|
||||
<div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card size="small" title="Token Information">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text strong>Token Type:</Text>
|
||||
<div>
|
||||
<Tag color="blue">{verificationResult.token_type.toUpperCase()}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{verificationResult.user_id && (
|
||||
<div>
|
||||
<Text strong>User ID:</Text>
|
||||
<div>{verificationResult.user_id}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verificationResult.expires_at && (
|
||||
<div>
|
||||
<Text strong>Expires At:</Text>
|
||||
<div>{new Date(verificationResult.expires_at).toLocaleString()}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verificationResult.max_valid_at && (
|
||||
<div>
|
||||
<Text strong>Max Valid Until:</Text>
|
||||
<div>{new Date(verificationResult.max_valid_at).toLocaleString()}</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card size="small" title="Permissions">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{verificationResult.permissions && verificationResult.permissions.length > 0 ? (
|
||||
<div>
|
||||
<Text strong>Available Permissions:</Text>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
{verificationResult.permissions.map(permission => (
|
||||
<Tag key={permission} color="green" style={{ margin: '2px' }}>
|
||||
{permission}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary">No permissions available</Text>
|
||||
)}
|
||||
|
||||
{verificationResult.permission_results && Object.keys(verificationResult.permission_results).length > 0 && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Text strong>Permission Check Results:</Text>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
{Object.entries(verificationResult.permission_results).map(([permission, granted]) => (
|
||||
<div key={permission} style={{ marginBottom: '4px' }}>
|
||||
<Tag color={granted ? 'green' : 'red'}>
|
||||
{permission}: {granted ? 'GRANTED' : 'DENIED'}
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{verificationResult.claims && Object.keys(verificationResult.claims).length > 0 && (
|
||||
<Card size="small" title="Token Claims" style={{ marginTop: '16px' }}>
|
||||
<Row gutter={8}>
|
||||
{Object.entries(verificationResult.claims).map(([key, value]) => (
|
||||
<Col span={8} key={key} style={{ marginBottom: '8px' }}>
|
||||
<Text strong>{key}:</Text>
|
||||
<div>{value}</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Verification Error */}
|
||||
{verificationError && (
|
||||
<Card title="Verification Error">
|
||||
<Alert
|
||||
message="Token Verification Failed"
|
||||
description={verificationError}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* No Token or Error */}
|
||||
{!callbackData.token && !callbackData.error && (
|
||||
<Result
|
||||
status="warning"
|
||||
title="Invalid Callback"
|
||||
subTitle="This callback page expects to receive either a token or error information from the login flow."
|
||||
extra={
|
||||
<Button type="primary" onClick={goBackToTester}>
|
||||
Go Back to Token Tester
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenTesterCallback;
|
||||
811
kms/kms-frontend/src/components/Tokens.tsx
Normal file
811
kms/kms-frontend/src/components/Tokens.tsx
Normal file
@ -0,0 +1,811 @@
|
||||
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 [verifyResult, setVerifyResult] = useState<any>(null);
|
||||
const [verifyLoading, setVerifyLoading] = useState(false);
|
||||
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);
|
||||
|
||||
// More robust validation for app_id
|
||||
if (!values.app_id || values.app_id === 'undefined' || values.app_id === undefined) {
|
||||
console.error('Invalid app_id detected:', values.app_id);
|
||||
message.error('Please select an application');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that the app_id exists in our applications list
|
||||
const selectedApp = applications.find(app => app.app_id === values.app_id);
|
||||
if (!selectedApp) {
|
||||
console.error('Selected app_id not found in applications list:', values.app_id);
|
||||
message.error('Selected application is not valid. Please refresh and try again.');
|
||||
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);
|
||||
console.log('Token creation response:', response);
|
||||
setNewTokenResponse(response);
|
||||
message.success('Token created successfully');
|
||||
loadApplications();
|
||||
} catch (error) {
|
||||
console.error('Failed to create token:', error);
|
||||
message.error('Failed to create token');
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenVerifyModal = () => {
|
||||
verifyForm.resetFields();
|
||||
setVerifyResult(null);
|
||||
setVerifyLoading(false);
|
||||
setVerifyModalVisible(true);
|
||||
};
|
||||
|
||||
const handleVerifyToken = async (values: any) => {
|
||||
try {
|
||||
setVerifyLoading(true);
|
||||
|
||||
const verifyRequest: VerifyRequest = {
|
||||
app_id: values.app_id,
|
||||
// Remove explicit type - it will be auto-detected from token prefix
|
||||
token: values.token,
|
||||
permissions: values.permissions || [],
|
||||
};
|
||||
|
||||
console.log('Verifying token with request:', verifyRequest);
|
||||
const response = await apiService.verifyToken(verifyRequest);
|
||||
console.log('Token verification response:', response);
|
||||
|
||||
// Store the result in state to display in the modal
|
||||
setVerifyResult(response);
|
||||
|
||||
// Show success message
|
||||
if (response && response.valid) {
|
||||
message.success('Token verification completed successfully!', 3);
|
||||
} else {
|
||||
message.warning('Token verification completed - token is invalid', 3);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to verify token:', error);
|
||||
|
||||
// Store error result in state
|
||||
setVerifyResult({
|
||||
valid: false,
|
||||
error: error instanceof Error ? error.message : 'An unexpected error occurred while verifying the token.',
|
||||
errorDetails: {
|
||||
networkError: true,
|
||||
suggestions: [
|
||||
'Check your network connection',
|
||||
'Verify the token format is correct',
|
||||
'Ensure the selected application is correct',
|
||||
'Confirm the API server is running'
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
message.error('Failed to verify token. Please check your network connection and try again.');
|
||||
} finally {
|
||||
setVerifyLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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={handleOpenVerifyModal}
|
||||
>
|
||||
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()}
|
||||
confirmLoading={verifyLoading}
|
||||
width={800}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Alert
|
||||
message="Automatic Token Type Detection"
|
||||
description="The system will automatically detect if your token is a static token (KMST-, KMS2T-, etc.) or user token (KMSUT-, KMS2UT-, etc.) based on its prefix."
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
<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 (paste from Token Tester for user tokens, or from static token creation)"
|
||||
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>
|
||||
|
||||
{/* Verification Results */}
|
||||
{verifyResult && (
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{verifyResult.valid ? (
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
) : (
|
||||
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
)}
|
||||
Verification Results
|
||||
</div>
|
||||
</Title>
|
||||
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* Overall Status */}
|
||||
<Card size="small" style={{ backgroundColor: verifyResult.valid ? '#f6ffed' : '#fff2f0' }}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text strong>Token Status: </Text>
|
||||
{verifyResult.valid ? (
|
||||
<Tag color="green" icon={<CheckCircleOutlined />} style={{ fontSize: '14px' }}>
|
||||
VALID
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="red" icon={<ExclamationCircleOutlined />} style={{ fontSize: '14px' }}>
|
||||
INVALID
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{verifyResult.permitted !== undefined && (
|
||||
<div>
|
||||
<Text strong>Permission Status: </Text>
|
||||
{verifyResult.permitted ? (
|
||||
<Tag color="green" icon={<CheckCircleOutlined />} style={{ fontSize: '14px' }}>
|
||||
ALL PERMISSIONS GRANTED
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="orange" icon={<ExclamationCircleOutlined />} style={{ fontSize: '14px' }}>
|
||||
SOME PERMISSIONS DENIED
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verifyResult.token_type && (
|
||||
<div>
|
||||
<Text strong>Token Type: </Text>
|
||||
<Tag color="blue" style={{ fontSize: '14px' }}>
|
||||
{verifyResult.token_type.toUpperCase()}
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* Token Permissions */}
|
||||
{verifyResult.permissions && verifyResult.permissions.length > 0 && (
|
||||
<div>
|
||||
<Text strong style={{ fontSize: '16px' }}>Available Token Permissions:</Text>
|
||||
<div style={{ marginTop: '12px', padding: '12px', backgroundColor: '#fafafa', borderRadius: '6px' }}>
|
||||
<Space wrap>
|
||||
{verifyResult.permissions.map((permission: string) => (
|
||||
<Tag key={permission} color="blue" style={{ margin: '2px' }}>
|
||||
{permission}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Requested Permission Results */}
|
||||
{verifyResult.permission_results && Object.keys(verifyResult.permission_results).length > 0 && (
|
||||
<div>
|
||||
<Text strong style={{ fontSize: '16px' }}>Requested Permission Results:</Text>
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
{Object.entries(verifyResult.permission_results).map(([permission, granted]) => (
|
||||
<div key={permission} style={{
|
||||
marginBottom: '8px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: granted ? '#f6ffed' : '#fff2f0',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${granted ? '#b7eb8f' : '#ffccc7'}`
|
||||
}}>
|
||||
<Space>
|
||||
{granted ? (
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
) : (
|
||||
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
)}
|
||||
<Text strong>{permission}</Text>
|
||||
<Tag color={granted ? 'green' : 'red'} style={{ marginLeft: 'auto' }}>
|
||||
{granted ? 'GRANTED' : 'DENIED'}
|
||||
</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Information */}
|
||||
{verifyResult.error && (
|
||||
<Alert
|
||||
message="Verification Error"
|
||||
description={verifyResult.error}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginTop: '16px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Error Details with Suggestions */}
|
||||
{verifyResult.errorDetails && verifyResult.errorDetails.networkError && (
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
Please check:
|
||||
</Text>
|
||||
<ul style={{ marginTop: '8px', paddingLeft: '20px' }}>
|
||||
{verifyResult.errorDetails.suggestions.map((suggestion: string, index: number) => (
|
||||
<li key={index}>{suggestion}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Additional Information */}
|
||||
{(verifyResult.expires_at || verifyResult.max_valid_at) && (
|
||||
<div>
|
||||
<Text strong style={{ fontSize: '16px' }}>Token Timing Information:</Text>
|
||||
<div style={{ marginTop: '12px', padding: '12px', backgroundColor: '#fafafa', borderRadius: '6px' }}>
|
||||
{verifyResult.expires_at && (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<Text strong>Expires At: </Text>
|
||||
<Text code>{new Date(verifyResult.expires_at).toLocaleString()}</Text>
|
||||
</div>
|
||||
)}
|
||||
{verifyResult.max_valid_at && (
|
||||
<div>
|
||||
<Text strong>Max Valid Until: </Text>
|
||||
<Text code>{new Date(verifyResult.max_valid_at).toLocaleString()}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</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;
|
||||
416
kms/kms-frontend/src/components/Users.tsx
Normal file
416
kms/kms-frontend/src/components/Users.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user