364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
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">
|
|
<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; |