Files
skybridge/kms/web/src/components/TokenTester.tsx
2025-08-27 12:27:50 -04:00

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;