397 lines
13 KiB
TypeScript
397 lines
13 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
import {
|
|
Card,
|
|
Button,
|
|
Space,
|
|
Typography,
|
|
Alert,
|
|
Row,
|
|
Col,
|
|
Tag,
|
|
Spin,
|
|
Result,
|
|
Input,
|
|
} from 'antd';
|
|
import {
|
|
CheckCircleOutlined,
|
|
ExclamationCircleOutlined,
|
|
CopyOutlined,
|
|
ArrowLeftOutlined,
|
|
} from '@ant-design/icons';
|
|
import { apiService } from '../services/apiService';
|
|
|
|
const { Title, Text } = Typography;
|
|
const { TextArea } = Input;
|
|
|
|
interface CallbackData {
|
|
token?: string;
|
|
state?: string;
|
|
error?: string;
|
|
error_description?: string;
|
|
}
|
|
|
|
interface VerificationResult {
|
|
valid: boolean;
|
|
permitted: boolean;
|
|
user_id?: string;
|
|
permissions: string[];
|
|
permission_results?: Record<string, boolean>;
|
|
expires_at?: string;
|
|
max_valid_at?: string;
|
|
token_type: string;
|
|
claims?: Record<string, string>;
|
|
error?: string;
|
|
}
|
|
|
|
const TokenTesterCallback: React.FC = () => {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const [loading, setLoading] = useState(true);
|
|
const [callbackData, setCallbackData] = useState<CallbackData>({});
|
|
const [verificationResult, setVerificationResult] = useState<VerificationResult | null>(null);
|
|
const [verificationError, setVerificationError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
parseCallbackData();
|
|
}, [location]);
|
|
|
|
const parseCallbackData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Parse URL parameters
|
|
const urlParams = new URLSearchParams(location.search);
|
|
let token = urlParams.get('token') || undefined;
|
|
|
|
// If no token in URL, try to extract from auth_token cookie
|
|
if (!token) {
|
|
token = getCookie('auth_token') || undefined;
|
|
}
|
|
|
|
const data: CallbackData = {
|
|
token: token,
|
|
state: urlParams.get('state') || undefined,
|
|
error: urlParams.get('error') || undefined,
|
|
error_description: urlParams.get('error_description') || undefined,
|
|
};
|
|
|
|
setCallbackData(data);
|
|
|
|
// If we have a token, try to verify it
|
|
if (data.token && !data.error) {
|
|
await verifyToken(data.token);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error parsing callback data:', error);
|
|
setVerificationError('Failed to parse callback data');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Utility function to get cookie value by name
|
|
const getCookie = (name: string): string | null => {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) {
|
|
const cookieValue = parts.pop()?.split(';').shift();
|
|
return cookieValue || null;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const verifyToken = async (token: string) => {
|
|
try {
|
|
// We need to extract app_id from the state or make a best guess
|
|
// For now, we'll try to verify without specifying app_id
|
|
// In a real implementation, the app_id should be included in the state parameter
|
|
|
|
// Try to get app_id from localStorage if it was stored during the test
|
|
const testData = localStorage.getItem('token_tester_data');
|
|
let appId = '';
|
|
|
|
if (testData) {
|
|
try {
|
|
const parsed = JSON.parse(testData);
|
|
appId = parsed.app_id || '';
|
|
} catch (e) {
|
|
console.warn('Could not parse stored test data');
|
|
}
|
|
}
|
|
|
|
if (!appId) {
|
|
// If we don't have app_id, we can't verify the token properly
|
|
setVerificationError('Cannot verify token: Application ID not found in callback state');
|
|
return;
|
|
}
|
|
|
|
const verifyRequest = {
|
|
app_id: appId,
|
|
token: token,
|
|
permissions: [], // We'll verify without specific permissions
|
|
};
|
|
|
|
const result = await apiService.verifyToken(verifyRequest);
|
|
setVerificationResult(result);
|
|
|
|
} catch (error: any) {
|
|
console.error('Token verification failed:', error);
|
|
setVerificationError(
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Token verification failed'
|
|
);
|
|
}
|
|
};
|
|
|
|
const copyToClipboard = (text: string) => {
|
|
navigator.clipboard.writeText(text);
|
|
};
|
|
|
|
const goBackToTester = () => {
|
|
navigate('/token-tester');
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
minHeight: '400px'
|
|
}}>
|
|
<Spin size="large" />
|
|
<Text style={{ marginLeft: '16px' }}>Processing callback...</Text>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 - Callback</Title>
|
|
<Text type="secondary">
|
|
Callback page for testing the login flow
|
|
</Text>
|
|
</div>
|
|
<Button
|
|
icon={<ArrowLeftOutlined />}
|
|
onClick={goBackToTester}
|
|
>
|
|
Back to Tester
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Callback Status */}
|
|
<Card title="Callback Status">
|
|
{callbackData.error ? (
|
|
<Alert
|
|
message="Callback Error"
|
|
description={`${callbackData.error}: ${callbackData.error_description || 'No description provided'}`}
|
|
type="error"
|
|
showIcon
|
|
/>
|
|
) : callbackData.token ? (
|
|
<Alert
|
|
message="Callback Successful"
|
|
description="Token received successfully from the login flow"
|
|
type="success"
|
|
showIcon
|
|
/>
|
|
) : (
|
|
<Alert
|
|
message="Invalid Callback"
|
|
description="No token or error information found in callback URL"
|
|
type="warning"
|
|
showIcon
|
|
/>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Token Information */}
|
|
{callbackData.token && (
|
|
<Card title="Received Token">
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
<div>
|
|
<Text strong>Token:</Text>
|
|
<TextArea
|
|
value={callbackData.token}
|
|
readOnly
|
|
rows={4}
|
|
style={{ fontFamily: 'monospace', fontSize: '12px', marginTop: '8px' }}
|
|
/>
|
|
<Button
|
|
size="small"
|
|
icon={<CopyOutlined />}
|
|
onClick={() => copyToClipboard(callbackData.token!)}
|
|
style={{ marginTop: '8px' }}
|
|
>
|
|
Copy Token
|
|
</Button>
|
|
</div>
|
|
|
|
{callbackData.state && (
|
|
<div>
|
|
<Text strong>State:</Text>
|
|
<div style={{ marginTop: '4px' }}>
|
|
<Text code>{callbackData.state}</Text>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Token Verification Results */}
|
|
{verificationResult && (
|
|
<Card
|
|
title={
|
|
<Space>
|
|
{verificationResult.valid ? (
|
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
|
) : (
|
|
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
|
)}
|
|
Token Verification Results
|
|
</Space>
|
|
}
|
|
>
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
<Alert
|
|
message={verificationResult.valid ? 'Token Valid' : 'Token Invalid'}
|
|
description={verificationResult.valid
|
|
? 'The token was successfully verified'
|
|
: verificationResult.error || 'Token verification failed'
|
|
}
|
|
type={verificationResult.valid ? 'success' : 'error'}
|
|
showIcon
|
|
/>
|
|
|
|
{verificationResult.valid && (
|
|
<div>
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Card size="small" title="Token Information">
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
<div>
|
|
<Text strong>Token Type:</Text>
|
|
<div>
|
|
<Tag color="blue">{verificationResult.token_type.toUpperCase()}</Tag>
|
|
</div>
|
|
</div>
|
|
|
|
{verificationResult.user_id && (
|
|
<div>
|
|
<Text strong>User ID:</Text>
|
|
<div>{verificationResult.user_id}</div>
|
|
</div>
|
|
)}
|
|
|
|
{verificationResult.expires_at && (
|
|
<div>
|
|
<Text strong>Expires At:</Text>
|
|
<div>{new Date(verificationResult.expires_at).toLocaleString()}</div>
|
|
</div>
|
|
)}
|
|
|
|
{verificationResult.max_valid_at && (
|
|
<div>
|
|
<Text strong>Max Valid Until:</Text>
|
|
<div>{new Date(verificationResult.max_valid_at).toLocaleString()}</div>
|
|
</div>
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
</Col>
|
|
|
|
<Col span={12}>
|
|
<Card size="small" title="Permissions">
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
{verificationResult.permissions && verificationResult.permissions.length > 0 ? (
|
|
<div>
|
|
<Text strong>Available Permissions:</Text>
|
|
<div style={{ marginTop: '8px' }}>
|
|
{verificationResult.permissions.map(permission => (
|
|
<Tag key={permission} color="green" style={{ margin: '2px' }}>
|
|
{permission}
|
|
</Tag>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Text type="secondary">No permissions available</Text>
|
|
)}
|
|
|
|
{verificationResult.permission_results && Object.keys(verificationResult.permission_results).length > 0 && (
|
|
<div style={{ marginTop: '16px' }}>
|
|
<Text strong>Permission Check Results:</Text>
|
|
<div style={{ marginTop: '8px' }}>
|
|
{Object.entries(verificationResult.permission_results).map(([permission, granted]) => (
|
|
<div key={permission} style={{ marginBottom: '4px' }}>
|
|
<Tag color={granted ? 'green' : 'red'}>
|
|
{permission}: {granted ? 'GRANTED' : 'DENIED'}
|
|
</Tag>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
{verificationResult.claims && Object.keys(verificationResult.claims).length > 0 && (
|
|
<Card size="small" title="Token Claims" style={{ marginTop: '16px' }}>
|
|
<Row gutter={8}>
|
|
{Object.entries(verificationResult.claims).map(([key, value]) => (
|
|
<Col span={8} key={key} style={{ marginBottom: '8px' }}>
|
|
<Text strong>{key}:</Text>
|
|
<div>{value}</div>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Space>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Verification Error */}
|
|
{verificationError && (
|
|
<Card title="Verification Error">
|
|
<Alert
|
|
message="Token Verification Failed"
|
|
description={verificationError}
|
|
type="error"
|
|
showIcon
|
|
/>
|
|
</Card>
|
|
)}
|
|
|
|
{/* No Token or Error */}
|
|
{!callbackData.token && !callbackData.error && (
|
|
<Result
|
|
status="warning"
|
|
title="Invalid Callback"
|
|
subTitle="This callback page expects to receive either a token or error information from the login flow."
|
|
extra={
|
|
<Button type="primary" onClick={goBackToTester}>
|
|
Go Back to Token Tester
|
|
</Button>
|
|
}
|
|
/>
|
|
)}
|
|
</Space>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TokenTesterCallback;
|