diff --git a/frontend/src/app/auth/github-popup/page.tsx b/frontend/src/app/auth/github-popup/page.tsx new file mode 100644 index 00000000..837a0228 --- /dev/null +++ b/frontend/src/app/auth/github-popup/page.tsx @@ -0,0 +1,207 @@ +'use client'; + +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(); + + // 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.closed) { + window.opener.postMessage(message, window.location.origin); + } + } catch (err) { + console.error('Failed to post message to opener:', err); + } + }; + + 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 () => { + try { + const urlParams = new URLSearchParams(window.location.search); + const isCallback = urlParams.has('code'); + const hasError = urlParams.has('error'); + + // 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 (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'); + + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { + redirectTo: `${window.location.origin}/auth/github-popup`, + queryParams: { + access_type: 'online', + prompt: 'select_account', + }, + }, + }); + + if (error) { + throw error; + } + } + } catch (err: any) { + 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 ( +
+
+ {status !== 'error' && ( + + )} + +
+

GitHub Sign-In

+

{getStatusMessage()}

+
+ + {status === 'error' && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/auth/page.tsx b/frontend/src/app/auth/page.tsx index b90c143b..c3599390 100644 --- a/frontend/src/app/auth/page.tsx +++ b/frontend/src/app/auth/page.tsx @@ -28,6 +28,7 @@ import { DialogDescription, DialogFooter, } from '@/components/ui/dialog'; +import GitHubSignIn from '@/components/GithubSignIn'; function LoginContent() { const router = useRouter(); @@ -387,11 +388,17 @@ function LoginContent() { )} - {/* Google Sign In */} -
- + {/* OAuth Sign In */} +
+
+ +
+
+ +
+ {/* Divider */}
diff --git a/frontend/src/components/GithubSignIn.tsx b/frontend/src/components/GithubSignIn.tsx new file mode 100644 index 00000000..e2207b02 --- /dev/null +++ b/frontend/src/components/GithubSignIn.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { useTheme } from 'next-themes'; +import { toast } from 'sonner'; +import { Icons } from './home/icons'; + +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(); + + // Cleanup function to handle auth state + const cleanupAuthState = useCallback(() => { + sessionStorage.removeItem('isGitHubAuthInProgress'); + setIsLoading(false); + }, []); + + // 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; + } + + // 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', handleMessage); + + return () => { + window.removeEventListener('message', handleMessage); + }; + }, [handleSuccess, handleError]); + + // Cleanup on component unmount + useEffect(() => { + return () => { + cleanupAuthState(); + }; + }, [cleanupAuthState]); + + const handleGitHubSignIn = async () => { + if (isLoading) return; + + let popupInterval: NodeJS.Timeout | null = null; + + try { + setIsLoading(true); + + // Store return URL for the popup + if (returnUrl) { + sessionStorage.setItem('github-returnUrl', returnUrl || '/dashboard'); + } + + // 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')) { + cleanupAuthState(); + toast.error('GitHub sign-in was cancelled or not completed.'); + } + }, 500); + } + }, 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 + + ); +} \ No newline at end of file diff --git a/frontend/src/components/home/icons.tsx b/frontend/src/components/home/icons.tsx index 7e4e693a..44612fd8 100644 --- a/frontend/src/components/home/icons.tsx +++ b/frontend/src/components/home/icons.tsx @@ -2461,4 +2461,25 @@ export const Icons = { ), -}; + github: ({ + className, + color = 'currentColor', + }: { + className?: string; + color?: string; + }) => ( + + ), +}; \ No newline at end of file diff --git a/frontend/src/components/home/sections/hero-section.tsx b/frontend/src/components/home/sections/hero-section.tsx index e938eccc..de2e9d08 100644 --- a/frontend/src/components/home/sections/hero-section.tsx +++ b/frontend/src/components/home/sections/hero-section.tsx @@ -31,6 +31,7 @@ import { useAccounts } from '@/hooks/use-accounts'; import { isLocalMode, config } from '@/lib/config'; import { toast } from 'sonner'; import { useModal } from '@/hooks/use-modal-store'; +import GitHubSignIn from '@/components/GithubSignIn'; import { ChatInput, ChatInputHandles } from '@/components/thread/chat-input/chat-input'; import { normalizeFilenameToNFC } from '@/lib/utils/unicode'; import { createQueryHook } from '@/hooks/use-query'; @@ -354,9 +355,10 @@ export function HeroSection() { - {/* Google Sign In */} + {/* OAuth Sign In */}
+
{/* Divider */}