diff --git a/frontend/src/app/auth/github-popup/page.tsx b/frontend/src/app/auth/github-popup/page.tsx index 72938071..837a0228 100644 --- a/frontend/src/app/auth/github-popup/page.tsx +++ b/frontend/src/app/auth/github-popup/page.tsx @@ -1,80 +1,207 @@ 'use client'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { createClient } from '@/lib/supabase/client'; +import { Loader2 } from 'lucide-react'; + +interface AuthMessage { + type: 'github-auth-success' | 'github-auth-error'; + message?: string; + returnUrl?: string; +} export default function GitHubOAuthPopup() { + const [status, setStatus] = useState<'loading' | 'processing' | 'error'>( + 'loading', + ); + const [errorMessage, setErrorMessage] = useState(''); + useEffect(() => { const supabase = createClient(); - const returnUrl = sessionStorage.getItem('returnUrl') || '/dashboard'; - const finish = (type: 'success' | 'error', message?: string) => { + // Get return URL from sessionStorage (set by parent component) + const returnUrl = + sessionStorage.getItem('github-returnUrl') || '/dashboard'; + + const postMessage = (message: AuthMessage) => { try { - if (window.opener) { - window.opener.postMessage( - type === 'success' - ? { type: 'github-auth-success', returnUrl } - : { - type: 'github-auth-error', - message: message || 'GitHub sign-in failed', - }, - window.opener.origin || '*', - ); + if (window.opener && !window.opener.closed) { + window.opener.postMessage(message, window.location.origin); } } catch (err) { - console.warn('Failed to post message to opener:', err); + console.error('Failed to post message to opener:', err); } + }; - setTimeout(() => window.close(), 150); // Give time for message delivery + const handleSuccess = () => { + setStatus('processing'); + postMessage({ + type: 'github-auth-success', + returnUrl, + }); + + // Close popup after short delay + setTimeout(() => { + window.close(); + }, 500); + }; + + const handleError = (message: string) => { + setStatus('error'); + setErrorMessage(message); + postMessage({ + type: 'github-auth-error', + message, + }); + + // Close popup after delay to show error + setTimeout(() => { + window.close(); + }, 2000); }; const handleOAuth = async () => { - const isOAuthCallback = new URLSearchParams(window.location.search).has( - 'code', - ); + try { + const urlParams = new URLSearchParams(window.location.search); + const isCallback = urlParams.has('code'); + const hasError = urlParams.has('error'); - if (isOAuthCallback) { - try { - const { - data: { session }, - } = await supabase.auth.getSession(); + // Handle OAuth errors + if (hasError) { + const error = urlParams.get('error'); + const errorDescription = urlParams.get('error_description'); + throw new Error(errorDescription || error || 'GitHub OAuth error'); + } - if (session) { - return finish('success'); + if (isCallback) { + // This is the callback from GitHub + setStatus('processing'); + + try { + // Wait a moment for Supabase to process the session + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const { + data: { session }, + error, + } = await supabase.auth.getSession(); + + if (error) { + throw error; + } + + if (session?.user) { + handleSuccess(); + return; + } + + // If no session yet, listen for auth state change + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange(async (event, session) => { + if (event === 'SIGNED_IN' && session?.user) { + subscription.unsubscribe(); + handleSuccess(); + } else if (event === 'SIGNED_OUT') { + subscription.unsubscribe(); + handleError('Authentication failed - please try again'); + } + }); + + // Cleanup subscription after timeout + setTimeout(() => { + subscription.unsubscribe(); + handleError('Authentication timeout - please try again'); + }, 10000); // 10 second timeout + } catch (authError: any) { + console.error('Auth processing error:', authError); + handleError(authError.message || 'Authentication failed'); } + } else { + // Start the OAuth flow + setStatus('loading'); - // Fallback if session is not yet populated - supabase.auth.onAuthStateChange((_event, session) => { - if (session) finish('success'); + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { + redirectTo: `${window.location.origin}/auth/github-popup`, + queryParams: { + access_type: 'online', + prompt: 'select_account', + }, + }, }); - return; - } catch (err: any) { - console.error('Session error:', err); - return finish('error', err.message); + if (error) { + throw error; + } } - } - - // Start the GitHub OAuth flow - try { - await supabase.auth.signInWithOAuth({ - provider: 'github', - options: { - redirectTo: `${window.location.origin}/auth/github-popup`, - }, - }); } catch (err: any) { - console.error('OAuth start error:', err); - finish('error', err.message); + console.error('OAuth error:', err); + handleError(err.message || 'Failed to authenticate with GitHub'); } }; + // Cleanup sessionStorage when popup closes + const handleBeforeUnload = () => { + sessionStorage.removeItem('github-returnUrl'); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + // Start OAuth process handleOAuth(); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; }, []); + const getStatusMessage = () => { + switch (status) { + case 'loading': + return 'Starting GitHub authentication...'; + case 'processing': + return 'Completing sign-in...'; + case 'error': + return errorMessage || 'Authentication failed'; + default: + return 'Processing...'; + } + }; + + const getStatusColor = () => { + switch (status) { + case 'error': + return 'text-red-500'; + case 'processing': + return 'text-green-500'; + default: + return 'text-muted-foreground'; + } + }; + return ( -
- Completing GitHub sign-in... +
+
+ {status !== 'error' && ( + + )} + +
+

GitHub Sign-In

+

{getStatusMessage()}

+
+ + {status === 'error' && ( + + )} +
); -} +} \ No newline at end of file diff --git a/frontend/src/components/GithubSignIn.tsx b/frontend/src/components/GithubSignIn.tsx index 9dfc52f8..e2207b02 100644 --- a/frontend/src/components/GithubSignIn.tsx +++ b/frontend/src/components/GithubSignIn.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useTheme } from 'next-themes'; import { toast } from 'sonner'; import { Icons } from './home/icons'; @@ -9,82 +9,172 @@ interface GitHubSignInProps { returnUrl?: string; } +interface AuthMessage { + type: 'github-auth-success' | 'github-auth-error'; + message?: string; + returnUrl?: string; +} + export default function GitHubSignIn({ returnUrl }: GitHubSignInProps) { const [isLoading, setIsLoading] = useState(false); const { resolvedTheme } = useTheme(); - useEffect(() => { - const handler = (event: MessageEvent) => { - if (event.origin !== window.location.origin) return; + // Cleanup function to handle auth state + const cleanupAuthState = useCallback(() => { + sessionStorage.removeItem('isGitHubAuthInProgress'); + setIsLoading(false); + }, []); - if (event.data?.type === 'github-auth-success') { - sessionStorage.removeItem('isGitHubAuthInProgress'); - setIsLoading(false); - window.location.href = - event.data.returnUrl || returnUrl || '/dashboard'; + // Handle success message + const handleSuccess = useCallback( + (data: AuthMessage) => { + cleanupAuthState(); + + // Add a small delay to ensure state is properly cleared + setTimeout(() => { + window.location.href = data.returnUrl || returnUrl || '/dashboard'; + }, 100); + }, + [cleanupAuthState, returnUrl], + ); + + // Handle error message + const handleError = useCallback( + (data: AuthMessage) => { + cleanupAuthState(); + toast.error(data.message || 'GitHub sign-in failed. Please try again.'); + }, + [cleanupAuthState], + ); + + // Message event handler + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + // Security: Only accept messages from same origin + if (event.origin !== window.location.origin) { + console.warn( + 'Rejected message from unauthorized origin:', + event.origin, + ); + return; } - if (event.data?.type === 'github-auth-error') { - sessionStorage.removeItem('isGitHubAuthInProgress'); - setIsLoading(false); - toast.error(event.data.message || 'GitHub sign-in failed.'); + // Validate message structure + if (!event.data?.type || typeof event.data.type !== 'string') { + return; + } + + switch (event.data.type) { + case 'github-auth-success': + handleSuccess(event.data); + break; + case 'github-auth-error': + handleError(event.data); + break; + default: + // Ignore unknown message types + break; } }; - window.addEventListener('message', handler); - return () => window.removeEventListener('message', handler); - }, [returnUrl]); + window.addEventListener('message', handleMessage); - const handleGitHubSignIn = () => { - const popup = window.open( - `${window.location.origin}/auth/github-popup`, - 'GitHubOAuth', - 'width=500,height=600', - ); + return () => { + window.removeEventListener('message', handleMessage); + }; + }, [handleSuccess, handleError]); - if (!popup) { - toast.error('Popup was blocked. Please enable popups and try again.'); - return; - } + // Cleanup on component unmount + useEffect(() => { + return () => { + cleanupAuthState(); + }; + }, [cleanupAuthState]); - setTimeout(() => { - sessionStorage.setItem('isGitHubAuthInProgress', '1'); + const handleGitHubSignIn = async () => { + if (isLoading) return; + + let popupInterval: NodeJS.Timeout | null = null; + + try { setIsLoading(true); - const interval = setInterval(() => { - if (popup.closed) { - clearInterval(interval); - setIsLoading(false); + // Store return URL for the popup + if (returnUrl) { + sessionStorage.setItem('github-returnUrl', returnUrl || '/dashboard'); + } - // Delay toast to allow success postMessage to clear the flag + // Open popup with proper dimensions and features + const popup = window.open( + `${window.location.origin}/auth/github-popup`, + 'GitHubOAuth', + 'width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes', + ); + + if (!popup) { + throw new Error( + 'Popup was blocked. Please enable popups and try again.', + ); + } + + // Set loading state and track popup + sessionStorage.setItem('isGitHubAuthInProgress', '1'); + + // Monitor popup closure + popupInterval = setInterval(() => { + if (popup.closed) { + if (popupInterval) { + clearInterval(popupInterval); + popupInterval = null; + } + + // Small delay to allow postMessage to complete setTimeout(() => { if (sessionStorage.getItem('isGitHubAuthInProgress')) { - sessionStorage.removeItem('isGitHubAuthInProgress'); - toast.error('GitHub sign-in was not completed.'); + cleanupAuthState(); + toast.error('GitHub sign-in was cancelled or not completed.'); } - }, 300); + }, 500); } - }, 500); - }, 0); + }, 1000); + } catch (error) { + console.error('GitHub sign-in error:', error); + if (popupInterval) { + clearInterval(popupInterval); + } + cleanupAuthState(); + toast.error( + error instanceof Error + ? error.message + : 'Failed to start GitHub sign-in', + ); + } }; return ( + // Matched the button with the GoogleSignIn component +
+
+ {isLoading ? ( +
+ ) : ( + + )} +
+
+ + {isLoading ? 'Signing in...' : 'Continue with GitHub'} + + ); -} +} \ No newline at end of file