Files
skybridge/kms-frontend/src/components/TokenTester.tsx
2025-08-23 16:48:31 -04:00

568 lines
18 KiB
TypeScript

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;
permissions?: 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();
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
);
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);
// Pre-fill the callback form with the token from the redirect URL
const urlParams = new URLSearchParams(response.redirect_url.split('?')[1]);
const token = urlParams.get('token');
if (token) {
callbackForm.setFieldsValue({
app_id: values.app_id,
token: token,
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,
permissions: verifyResponse.permissions,
error: verifyResponse.error,
timestamp: new Date().toISOString(),
};
setCallbackResult(result);
setCurrentStep(4);
if (verifyResponse.valid) {
message.success('Callback test completed successfully!');
} else {
message.warning('Callback test completed - token verification failed');
}
} 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');
} finally {
setCallbackLoading(false);
}
};
const resetTest = () => {
setCurrentStep(0);
setLoginResult(null);
setCallbackResult(null);
setCallbackModalVisible(false);
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>
<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>Use callback URL (test full flow)</Checkbox>
</Form.Item>
</Col>
</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 && callbackResult.permissions && (
<div>
<Text strong>Verified Permissions:</Text>
<div style={{ marginTop: '8px' }}>
{callbackResult.permissions.map(permission => (
<Tag key={permission} color="green" style={{ margin: '2px' }}>
{permission}
</Tag>
))}
</div>
</div>
)}
<div>
<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="The login flow returned a redirect URL with a token. Test the callback handling by verifying the token."
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;