Files
skybridge/kms-frontend/src/components/TokenTesterCallback.tsx
2025-08-23 23:15:30 -04:00

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;