mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
- Updated the message handler to only accept messages from Telegram OAuth, ensuring better security. - Refactored the authentication process to handle Telegram's auth_result more effectively, including parsing the payload and managing session tokens. - Improved error handling during the authentication process, providing clearer error messages for failed operations. - Added a new allowed host in vite.config.ts for the frontend runner. These changes streamline the authentication process with Telegram, enhancing security and user experience.
314 lines
12 KiB
TypeScript
314 lines
12 KiB
TypeScript
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { BACKEND_URL, TELEGRAM_BOT_ID } from '../utils/config';
|
|
import { useAppDispatch } from '../store/hooks';
|
|
import { setToken } from '../store/authSlice';
|
|
|
|
interface TelegramAuthData {
|
|
id: number;
|
|
first_name?: string;
|
|
last_name?: string;
|
|
username?: string;
|
|
photo_url?: string;
|
|
auth_date: number;
|
|
hash: string;
|
|
}
|
|
|
|
interface TelegramLoginButtonProps {
|
|
onSuccess?: () => void;
|
|
className?: string;
|
|
text?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
const TelegramLoginButton = ({
|
|
onSuccess,
|
|
className = '',
|
|
text = 'Yes, Login with Telegram',
|
|
disabled: externalDisabled = false,
|
|
}: TelegramLoginButtonProps) => {
|
|
const navigate = useNavigate();
|
|
const dispatch = useAppDispatch();
|
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
|
|
const handleTelegramLogin = async () => {
|
|
if (isAuthenticating || externalDisabled) return;
|
|
|
|
setIsAuthenticating(true);
|
|
|
|
try {
|
|
const origin = window.location.origin;
|
|
const botId = TELEGRAM_BOT_ID;
|
|
// Pass origin as query param so backend can use it for postMessage
|
|
const callbackUrl = `${BACKEND_URL}/auth/telegram/callback?frontend_origin=${encodeURIComponent(origin)}`;
|
|
const oauthUrl = `https://oauth.telegram.org/auth?bot_id=${botId}&origin=${encodeURIComponent(origin)}&request_access=write&embed=0&return_to=${encodeURIComponent(callbackUrl)}`;
|
|
|
|
// Open popup window
|
|
const width = 550;
|
|
const height = 470;
|
|
const availLeft = 'availLeft' in screen ? (screen as { availLeft: number }).availLeft : 0;
|
|
const availTop = 'availTop' in screen ? (screen as { availTop: number }).availTop : 0;
|
|
const left = Math.max(0, (screen.width - width) / 2) + availLeft;
|
|
const top = Math.max(0, (screen.height - height) / 2) + availTop;
|
|
|
|
const popup = window.open(
|
|
oauthUrl,
|
|
'telegram_oauth',
|
|
`width=${width},height=${height},left=${left},top=${top},status=0,location=0,menubar=0,toolbar=0`
|
|
);
|
|
|
|
if (!popup) {
|
|
throw new Error('Failed to open popup window. Please allow popups for this site.');
|
|
}
|
|
|
|
let authCompleted = false;
|
|
|
|
// Listen for postMessage from Telegram OAuth
|
|
const messageHandler = async (event: MessageEvent) => {
|
|
// 1) Only accept messages from Telegram OAuth
|
|
if (event.origin !== 'https://oauth.telegram.org') {
|
|
return;
|
|
}
|
|
|
|
// 2) Parse the payload (Telegram sends a JSON string)
|
|
let data: any;
|
|
try {
|
|
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
// 3) Handle successful auth_result from Telegram
|
|
if (data.event === 'auth_result' && data.result && !authCompleted) {
|
|
authCompleted = true;
|
|
window.removeEventListener('message', messageHandler);
|
|
popup.close();
|
|
|
|
const telegramData: TelegramAuthData = {
|
|
id: data.result.id,
|
|
first_name: data.result.first_name,
|
|
last_name: data.result.last_name,
|
|
username: data.result.username,
|
|
photo_url: data.result.photo_url,
|
|
auth_date: data.result.auth_date,
|
|
hash: data.result.hash,
|
|
};
|
|
|
|
try {
|
|
// Send Telegram auth data to backend to verify and exchange for JWT
|
|
const webCompleteResponse = await fetch(`${BACKEND_URL}/auth/web-complete`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
method: 'telegram',
|
|
telegramUser: telegramData,
|
|
}),
|
|
});
|
|
|
|
if (!webCompleteResponse.ok) {
|
|
const errorData = await webCompleteResponse.json().catch(() => ({}));
|
|
throw new Error(errorData.error || 'Failed to complete authentication');
|
|
}
|
|
|
|
const { data: completeData } = await webCompleteResponse.json();
|
|
const { loginToken } = completeData || {};
|
|
|
|
if (!loginToken) {
|
|
throw new Error('No login token received from server');
|
|
}
|
|
|
|
const exchangeResponse = await fetch(`${BACKEND_URL}/auth/desktop-exchange`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
token: loginToken,
|
|
}),
|
|
});
|
|
|
|
if (!exchangeResponse.ok) {
|
|
const errorData = await exchangeResponse.json().catch(() => ({}));
|
|
throw new Error(errorData.error || 'Failed to exchange token');
|
|
}
|
|
|
|
const exchangeData = await exchangeResponse.json();
|
|
const { sessionToken } = exchangeData.data || {};
|
|
|
|
if (!sessionToken) {
|
|
throw new Error('No JWT token received from server');
|
|
}
|
|
|
|
// Store JWT token in store (this is the JWT from the backend)
|
|
dispatch(setToken(sessionToken));
|
|
|
|
// Call onSuccess callback if provided, otherwise navigate to onboarding
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
} else {
|
|
navigate('/onboarding/step1');
|
|
}
|
|
setIsAuthenticating(false);
|
|
} catch (error) {
|
|
setIsAuthenticating(false);
|
|
console.error('Failed to complete Telegram authentication:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', messageHandler);
|
|
|
|
// Fallback: Try to extract auth data from popup URL if postMessage doesn't work
|
|
// This will only work if the popup redirects to our origin (unlikely but possible)
|
|
const checkPopupUrl = setInterval(async () => {
|
|
if (authCompleted || popup.closed) {
|
|
clearInterval(checkPopupUrl);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Try to read popup location (will throw if cross-origin)
|
|
const popupUrl = popup.location.href;
|
|
|
|
// If popup is on our callback URL, extract data and get JWT from backend
|
|
if (popupUrl.startsWith(callbackUrl.split('?')[0])) {
|
|
const url = new URL(popupUrl);
|
|
const telegramData: TelegramAuthData = {
|
|
id: parseInt(url.searchParams.get('id') || '0', 10),
|
|
first_name: url.searchParams.get('first_name') || undefined,
|
|
last_name: url.searchParams.get('last_name') || undefined,
|
|
username: url.searchParams.get('username') || undefined,
|
|
photo_url: url.searchParams.get('photo_url') || undefined,
|
|
auth_date: parseInt(url.searchParams.get('auth_date') || '0', 10),
|
|
hash: url.searchParams.get('hash') || '',
|
|
};
|
|
|
|
// Validate required fields
|
|
if (telegramData.id && telegramData.auth_date && telegramData.hash) {
|
|
authCompleted = true;
|
|
clearInterval(checkPopupUrl);
|
|
popup.close();
|
|
window.removeEventListener('message', messageHandler);
|
|
|
|
try {
|
|
// Send Telegram auth data to backend to get JWT token
|
|
// Use web-complete to verify and then exchange for JWT
|
|
const webCompleteResponse = await fetch(`${BACKEND_URL}/auth/web-complete`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
method: 'telegram',
|
|
telegramUser: telegramData,
|
|
}),
|
|
});
|
|
|
|
if (!webCompleteResponse.ok) {
|
|
const errorData = await webCompleteResponse.json().catch(() => ({}));
|
|
throw new Error(errorData.error || 'Failed to complete authentication');
|
|
}
|
|
|
|
const { data } = await webCompleteResponse.json();
|
|
const { loginToken } = data;
|
|
|
|
if (!loginToken) {
|
|
throw new Error('No login token received from server');
|
|
}
|
|
|
|
// Exchange handoff token for JWT session token
|
|
const exchangeResponse = await fetch(`${BACKEND_URL}/auth/desktop-exchange`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
token: loginToken,
|
|
}),
|
|
});
|
|
|
|
if (!exchangeResponse.ok) {
|
|
const errorData = await exchangeResponse.json().catch(() => ({}));
|
|
throw new Error(errorData.error || 'Failed to exchange token');
|
|
}
|
|
|
|
const exchangeData = await exchangeResponse.json();
|
|
const { sessionToken } = exchangeData.data;
|
|
|
|
if (!sessionToken) {
|
|
throw new Error('No JWT token received from server');
|
|
}
|
|
|
|
// Store JWT token in store (this is the JWT from the backend)
|
|
dispatch(setToken(sessionToken));
|
|
|
|
// Call onSuccess callback if provided, otherwise navigate to onboarding
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
} else {
|
|
navigate('/onboarding/step1');
|
|
}
|
|
setIsAuthenticating(false);
|
|
} catch (error) {
|
|
setIsAuthenticating(false);
|
|
console.error('Failed to complete authentication:', error);
|
|
// alert(error instanceof Error ? error.message : 'Authentication failed. Please try again.');
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Cross-origin or other error - this is expected, continue polling
|
|
// The postMessage handler will catch the auth completion
|
|
}
|
|
}, 500);
|
|
|
|
// Monitor popup closure
|
|
const checkClosed = setInterval(() => {
|
|
if (popup.closed && !authCompleted) {
|
|
clearInterval(checkClosed);
|
|
clearInterval(checkPopupUrl);
|
|
window.removeEventListener('message', messageHandler);
|
|
setIsAuthenticating(false);
|
|
}
|
|
}, 500);
|
|
|
|
// Timeout after 5 minutes
|
|
setTimeout(() => {
|
|
if (!authCompleted) {
|
|
if (!popup.closed) {
|
|
popup.close();
|
|
}
|
|
clearInterval(checkClosed);
|
|
clearInterval(checkPopupUrl);
|
|
window.removeEventListener('message', messageHandler);
|
|
setIsAuthenticating(false);
|
|
// alert('Authentication timed out. Please try again.');
|
|
}
|
|
}, 5 * 60 * 1000);
|
|
} catch (error) {
|
|
setIsAuthenticating(false);
|
|
console.error('Failed to start Telegram authentication:', error);
|
|
// alert(error instanceof Error ? error.message : 'Failed to start authentication. Please try again.');
|
|
}
|
|
};
|
|
|
|
const isDisabled = isAuthenticating || externalDisabled;
|
|
|
|
return (
|
|
<button
|
|
onClick={handleTelegramLogin}
|
|
disabled={isDisabled}
|
|
className={`w-full flex items-center justify-center space-x-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-semibold py-4 rounded-xl transition-all duration-300 hover:shadow-medium hover:scale-[1.02] active:scale-[0.98] disabled:hover:scale-100 ${className}`}
|
|
>
|
|
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
|
</svg>
|
|
<span>{isAuthenticating ? 'Authenticating...' : text}</span>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
export default TelegramLoginButton;
|