unified UI thing
This commit is contained in:
373
kms/web/src/components/TokenTester.tsx
Normal file
373
kms/web/src/components/TokenTester.tsx
Normal file
@ -0,0 +1,373 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Stack,
|
||||
Title,
|
||||
Card,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Button,
|
||||
Group,
|
||||
Text,
|
||||
Alert,
|
||||
Badge,
|
||||
Divider,
|
||||
Select,
|
||||
MultiSelect,
|
||||
Code,
|
||||
Grid,
|
||||
Loader,
|
||||
JsonInput,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconTestPipe,
|
||||
IconCheck,
|
||||
IconX,
|
||||
IconAlertCircle,
|
||||
IconClock,
|
||||
IconUser,
|
||||
IconKey,
|
||||
} from '@tabler/icons-react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
apiService,
|
||||
Application,
|
||||
VerifyRequest,
|
||||
VerifyResponse,
|
||||
} from '../services/apiService';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const TokenTester: React.FC = () => {
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [result, setResult] = useState<VerifyResponse | null>(null);
|
||||
|
||||
const form = useForm<VerifyRequest>({
|
||||
initialValues: {
|
||||
app_id: '',
|
||||
user_id: '',
|
||||
token: '',
|
||||
permissions: [],
|
||||
},
|
||||
validate: {
|
||||
app_id: (value) => value.length < 1 ? 'Application is required' : null,
|
||||
token: (value) => value.length < 1 ? 'Token is required' : null,
|
||||
},
|
||||
});
|
||||
|
||||
const availablePermissions = [
|
||||
'app.read',
|
||||
'app.write',
|
||||
'app.delete',
|
||||
'token.read',
|
||||
'token.create',
|
||||
'token.revoke',
|
||||
'repo.read',
|
||||
'repo.write',
|
||||
'repo.admin',
|
||||
'permission.read',
|
||||
'permission.write',
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadApplications();
|
||||
}, []);
|
||||
|
||||
const loadApplications = async () => {
|
||||
try {
|
||||
const response = await apiService.getApplications(100, 0);
|
||||
setApplications(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load applications:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: VerifyRequest) => {
|
||||
try {
|
||||
setTesting(true);
|
||||
setResult(null);
|
||||
|
||||
// Clean up the request - remove empty fields
|
||||
const cleanedValues = {
|
||||
...values,
|
||||
user_id: values.user_id || undefined,
|
||||
permissions: values.permissions && values.permissions.length > 0 ? values.permissions : undefined,
|
||||
};
|
||||
|
||||
const response = await apiService.verifyToken(cleanedValues);
|
||||
setResult(response);
|
||||
|
||||
if (response.valid) {
|
||||
notifications.show({
|
||||
title: 'Token Verified',
|
||||
message: `Token is ${response.permitted ? 'valid and permitted' : 'valid but not permitted'}`,
|
||||
color: response.permitted ? 'green' : 'orange',
|
||||
});
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Token Invalid',
|
||||
message: response.error || 'Token verification failed',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to verify token:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to verify token',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (result: VerifyResponse) => {
|
||||
if (!result.valid) return 'red';
|
||||
if (result.valid && result.permitted) return 'green';
|
||||
return 'orange';
|
||||
};
|
||||
|
||||
const getStatusIcon = (result: VerifyResponse) => {
|
||||
if (!result.valid) return IconX;
|
||||
if (result.valid && result.permitted) return IconCheck;
|
||||
return IconAlertCircle;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={2} mb="xs">
|
||||
Token Tester
|
||||
</Title>
|
||||
<Text c="dimmed">
|
||||
Test and verify API tokens against your applications
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<Card shadow="sm" radius="md" withBorder p="lg">
|
||||
<Title order={3} mb="md">
|
||||
Test Configuration
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
label="Application"
|
||||
placeholder="Select an application to test against"
|
||||
required
|
||||
data={applications.map(app => ({
|
||||
value: app.app_id,
|
||||
label: `${app.app_id} (${app.type.join(', ')})`,
|
||||
}))}
|
||||
{...form.getInputProps('app_id')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="User ID (Optional)"
|
||||
placeholder="user@example.com"
|
||||
description="Leave empty for token-only verification"
|
||||
{...form.getInputProps('user_id')}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Token"
|
||||
placeholder="Paste your token here..."
|
||||
required
|
||||
minRows={3}
|
||||
{...form.getInputProps('token')}
|
||||
/>
|
||||
|
||||
<MultiSelect
|
||||
label="Required Permissions (Optional)"
|
||||
placeholder="Select permissions to test"
|
||||
description="Leave empty to skip permission checks"
|
||||
data={availablePermissions.map(perm => ({
|
||||
value: perm,
|
||||
label: perm,
|
||||
}))}
|
||||
{...form.getInputProps('permissions')}
|
||||
searchable
|
||||
/>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
type="submit"
|
||||
loading={testing}
|
||||
leftSection={!testing ? <IconTestPipe size={16} /> : <Loader size={16} />}
|
||||
disabled={applications.length === 0}
|
||||
>
|
||||
{testing ? 'Testing...' : 'Test Token'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<Card shadow="sm" radius="md" withBorder p="lg" h="100%">
|
||||
<Title order={3} mb="md">
|
||||
Test Results
|
||||
</Title>
|
||||
|
||||
{!result && !testing && (
|
||||
<Stack align="center" justify="center" h={300}>
|
||||
<IconTestPipe size={48} color="gray" />
|
||||
<Text c="dimmed" ta="center">
|
||||
Configure your test parameters and click "Test Token" to see results
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{testing && (
|
||||
<Stack align="center" justify="center" h={300}>
|
||||
<Loader size="lg" />
|
||||
<Text>Verifying token...</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<Stack gap="md">
|
||||
<Alert
|
||||
icon={React.createElement(getStatusIcon(result), { size: 16 })}
|
||||
title={
|
||||
!result.valid
|
||||
? 'Token Invalid'
|
||||
: result.permitted
|
||||
? 'Token Valid & Permitted'
|
||||
: 'Token Valid but Not Permitted'
|
||||
}
|
||||
color={getStatusColor(result)}
|
||||
>
|
||||
{result.error || (
|
||||
result.valid && result.permitted
|
||||
? 'Token is valid and has the required permissions'
|
||||
: result.valid
|
||||
? 'Token is valid but lacks some required permissions'
|
||||
: 'Token verification failed'
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>Valid:</Text>
|
||||
<Badge color={result.valid ? 'green' : 'red'} variant="light">
|
||||
{result.valid ? 'Yes' : 'No'}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>Permitted:</Text>
|
||||
<Badge color={result.permitted ? 'green' : 'red'} variant="light">
|
||||
{result.permitted ? 'Yes' : 'No'}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>Token Type:</Text>
|
||||
<Badge variant="light">
|
||||
{result.token_type}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
{result.user_id && (
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>User ID:</Text>
|
||||
<Text size="sm" style={{ fontFamily: 'monospace' }}>
|
||||
{result.user_id}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{result.expires_at && (
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>Expires At:</Text>
|
||||
<Text size="sm">
|
||||
{dayjs(result.expires_at).format('MMM DD, YYYY HH:mm')}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{result.max_valid_at && (
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>Max Valid Until:</Text>
|
||||
<Text size="sm">
|
||||
{dayjs(result.max_valid_at).format('MMM DD, YYYY HH:mm')}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{result.permissions && result.permissions.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div>
|
||||
<Text fw={500} mb="xs">Token Permissions:</Text>
|
||||
<Group gap="xs">
|
||||
{result.permissions.map((perm) => (
|
||||
<Badge key={perm} variant="light" size="sm" color="blue">
|
||||
{perm}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{result.permission_results && Object.keys(result.permission_results).length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div>
|
||||
<Text fw={500} mb="xs">Permission Check Results:</Text>
|
||||
<Stack gap="xs">
|
||||
{Object.entries(result.permission_results).map(([permission, granted]) => (
|
||||
<Group key={permission} justify="space-between">
|
||||
<Text size="sm" style={{ fontFamily: 'monospace' }}>
|
||||
{permission}
|
||||
</Text>
|
||||
<Badge color={granted ? 'green' : 'red'} variant="light" size="sm">
|
||||
{granted ? 'Granted' : 'Denied'}
|
||||
</Badge>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{result.claims && Object.keys(result.claims).length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div>
|
||||
<Text fw={500} mb="xs">Token Claims:</Text>
|
||||
<Code block>
|
||||
{JSON.stringify(result.claims, null, 2)}
|
||||
</Code>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{applications.length === 0 && (
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="No Applications Found"
|
||||
color="yellow"
|
||||
>
|
||||
You need to create at least one application before you can test tokens.
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenTester;
|
||||
Reference in New Issue
Block a user