568 lines
18 KiB
TypeScript
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;
|