From db815802f7df55cf9dae5e94c4d7172cee1c3172 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 15 Aug 2025 12:33:49 +0100 Subject: [PATCH] loginTest --- frontend/package-lock.json | 117 ++++- frontend/package.json | 1 + frontend/src/App.tsx | 24 +- frontend/src/AuthExample.tsx | 171 +++++++ frontend/src/components/auth/RequireAuth.tsx | 45 ++ frontend/src/lib/supabase.ts | 58 +++ frontend/src/lib/useSession.tsx | 187 ++++++++ frontend/src/routes/AuthCallback.tsx | 186 ++++++++ frontend/src/routes/AuthDebug.tsx | 477 +++++++++++++++++++ frontend/src/routes/Login.tsx | 317 ++++++++++++ frontend/src/routes/LoginTest.tsx | 87 ++++ 11 files changed, 1661 insertions(+), 9 deletions(-) create mode 100644 frontend/src/AuthExample.tsx create mode 100644 frontend/src/components/auth/RequireAuth.tsx create mode 100644 frontend/src/lib/supabase.ts create mode 100644 frontend/src/lib/useSession.tsx create mode 100644 frontend/src/routes/AuthCallback.tsx create mode 100644 frontend/src/routes/AuthDebug.tsx create mode 100644 frontend/src/routes/Login.tsx create mode 100644 frontend/src/routes/LoginTest.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b2141beb7..7c29e9e65 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@mantine/hooks": "^8.0.1", "@mui/icons-material": "^7.1.0", "@mui/material": "^7.1.0", + "@supabase/supabase-js": "^2.55.0", "@tailwindcss/postcss": "^4.1.8", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", @@ -1918,6 +1919,102 @@ "dev": true, "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.71.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz", + "integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.5.tgz", + "integrity": "sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@supabase/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@supabase/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", + "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.1.tgz", + "integrity": "sha512-edRFa2IrQw50kNntvUyS38hsL7t2d/psah6om6aNTLLcWem0R6bOUq7sk7DsGeSlNfuwEwWn57FdYSva6VddYw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.11.0.tgz", + "integrity": "sha512-Y+kx/wDgd4oasAgoAq0bsbQojwQ+ejIif8uczZ9qufRHWFLMU5cODT+ApHsSrDufqUcVKt+eyxtOXSkeh2v9ww==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.55.0.tgz", + "integrity": "sha512-Y1uV4nEMjQV1x83DGn7+Z9LOisVVRlY1geSARrUHbXWgbyKLZ6/08dvc0Us1r6AJ4tcKpwpCZWG9yDQYo1JgHg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.71.1", + "@supabase/functions-js": "2.4.5", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.4", + "@supabase/realtime-js": "2.15.1", + "@supabase/storage-js": "^2.10.4" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz", @@ -2389,7 +2486,6 @@ "version": "24.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", - "dev": true, "dependencies": { "undici-types": "~7.10.0" } @@ -2400,6 +2496,12 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -2434,6 +2536,15 @@ "@types/react": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", @@ -7417,8 +7528,7 @@ "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" }, "node_modules/universalify": { "version": "2.0.1", @@ -8899,7 +9009,6 @@ "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/frontend/package.json b/frontend/package.json index b59be58e9..bfcd83f6d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@mantine/hooks": "^8.0.1", "@mui/icons-material": "^7.1.0", "@mui/material": "^7.1.0", + "@supabase/supabase-js": "^2.55.0", "@tailwindcss/postcss": "^4.1.8", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 852204b25..f6c36ca58 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,13 @@ import React from 'react'; +import { Routes, Route } from 'react-router-dom'; import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider'; import { FileContextProvider } from './contexts/FileContext'; import { FilesModalProvider } from './contexts/FilesModalContext'; +import { AuthProvider } from './lib/useSession'; import HomePage from './pages/HomePage'; +import Login from './routes/Login'; +import AuthCallback from './routes/AuthCallback'; +import AuthDebug from './routes/AuthDebug'; // Import global styles import './styles/tailwind.css'; @@ -11,11 +16,20 @@ import './index.css'; export default function App() { return ( - - - - - + + + + + } /> + } /> + } /> + } /> + {/* Catch-all route - redirects unknown paths to home */} + } /> + + + + ); } diff --git a/frontend/src/AuthExample.tsx b/frontend/src/AuthExample.tsx new file mode 100644 index 000000000..53fe04b90 --- /dev/null +++ b/frontend/src/AuthExample.tsx @@ -0,0 +1,171 @@ +import { createBrowserRouter, RouterProvider } from 'react-router-dom' +import { AuthProvider, useAuth } from './lib/useSession' +import { supabase } from './lib/supabase' +import RequireAuth from './components/auth/RequireAuth' +import Login from './routes/Login' +import AuthCallback from './routes/AuthCallback' +import AuthDebug from './routes/AuthDebug' + +// Example protected component +function ProtectedDashboard() { + const { session, user, signOut } = useAuth() + + return ( +
+
+
+
+
+

Dashboard

+

Welcome back!

+
+ +
+ +
+
+

Authentication Successful!

+

You are signed in as {user?.email}

+
+ +
+
+
User ID
+
{user?.id}
+
+
+
Email
+
{user?.email}
+
+
+
Provider
+
{user?.app_metadata?.provider}
+
+
+ +
+ + Full Session Data + +
+                {JSON.stringify(session, null, 2)}
+              
+
+
+
+
+
+ ) +} + +// Example home page +function HomePage() { + return ( +
+
+
+

+ Stirling PDF - Authentication Demo +

+

+ This is a demo of the Supabase authentication integration +

+ +
+ + Go to Login + + + Protected Dashboard + + + Debug Panel + +
+
+
+
+ ) +} + +// Router configuration +const router = createBrowserRouter([ + // Public routes + { path: '/', element: }, + { path: '/login', element: }, + { path: '/auth/callback', element: }, + { path: '/debug', element: }, + + // Protected routes + { + path: '/dashboard', + element: ( + + + + ) + }, +]) + +// Main App component with auth provider +export default function AuthExample() { + return ( + + + + ) +} + +// Additional utility functions for easy integration +export const authUtils = { + // Sign in with GitHub (can be called from anywhere) + signInWithGitHub: async (nextPath = '/') => { + const redirectTo = `${window.location.origin}/auth/callback?next=${encodeURIComponent(nextPath)}` + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { redirectTo } + }) + if (error) { + console.error('Sign in error:', error) + throw error + } + }, + + // Sign out (can be called from anywhere) + signOut: async () => { + const { error } = await supabase.auth.signOut() + if (error) { + console.error('Sign out error:', error) + throw error + } + }, + + // Get current session + getCurrentSession: async () => { + const { data, error } = await supabase.auth.getSession() + return { session: data.session, error } + }, + + // Check if user is authenticated + isAuthenticated: async () => { + const { session } = await authUtils.getCurrentSession() + return !!session + } +} + +// Import this in your main App.tsx or wherever you want to add auth +// import AuthExample from './AuthExample' \ No newline at end of file diff --git a/frontend/src/components/auth/RequireAuth.tsx b/frontend/src/components/auth/RequireAuth.tsx new file mode 100644 index 000000000..e5fddc2b3 --- /dev/null +++ b/frontend/src/components/auth/RequireAuth.tsx @@ -0,0 +1,45 @@ +import { ReactNode } from 'react' +import { Navigate, useLocation } from 'react-router-dom' +import { useAuth } from '../../lib/useSession' + +interface RequireAuthProps { + children: ReactNode + fallbackPath?: string +} + +export function RequireAuth({ children, fallbackPath = '/login' }: RequireAuthProps) { + const { session, loading, error } = useAuth() + const location = useLocation() + + console.log('[RequireAuth Debug] Auth check:', { + hasSession: !!session, + loading, + hasError: !!error, + currentPath: location.pathname, + fallbackPath + }) + + // Show loading spinner while checking auth + if (loading) { + return ( +
+
+
+

Checking authentication...

+
+
+ ) + } + + // Redirect to login if not authenticated + if (!session) { + const redirectPath = `${fallbackPath}?next=${encodeURIComponent(location.pathname + location.search)}` + console.log('[RequireAuth Debug] Redirecting to login:', redirectPath) + return + } + + // Render protected content + return <>{children} +} + +export default RequireAuth \ No newline at end of file diff --git a/frontend/src/lib/supabase.ts b/frontend/src/lib/supabase.ts new file mode 100644 index 000000000..9fe9c80ac --- /dev/null +++ b/frontend/src/lib/supabase.ts @@ -0,0 +1,58 @@ +import { createClient } from '@supabase/supabase-js' + +// Debug helper to log Supabase configuration +const debugConfig = () => { + const url = import.meta.env.VITE_SUPABASE_URL + const key = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY + + console.log('[Supabase Debug] Configuration:', { + url: url ? '✓ URL configured' : '✗ URL missing', + key: key ? '✓ Key configured' : '✗ Key missing', + urlValue: url || 'undefined', + keyValue: key ? `${key.substring(0, 20)}...` : 'undefined' + }) + + return { url, key } +} + +const config = debugConfig() + +if (!config.url) { + throw new Error('Missing VITE_SUPABASE_URL environment variable') +} + +if (!config.key) { + throw new Error('Missing VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY environment variable') +} + +export const supabase = createClient( + config.url, + config.key, + { + auth: { + persistSession: true, // keep session in localStorage + autoRefreshToken: true, + detectSessionInUrl: true, // helpful on first load after redirect + debug: import.meta.env.DEV, // Enable debug logs in development + }, + } +) + +// Debug helper for auth events +export const debugAuthEvents = () => { + supabase.auth.onAuthStateChange((event, session) => { + console.log('[Supabase Debug] Auth state change:', { + event, + hasSession: !!session, + userId: session?.user?.id, + email: session?.user?.email, + provider: session?.user?.app_metadata?.provider, + timestamp: new Date().toISOString() + }) + }) +} + +// Call this in development to enable auth debugging +if (import.meta.env.DEV) { + debugAuthEvents() +} \ No newline at end of file diff --git a/frontend/src/lib/useSession.tsx b/frontend/src/lib/useSession.tsx new file mode 100644 index 000000000..e80a7f1e5 --- /dev/null +++ b/frontend/src/lib/useSession.tsx @@ -0,0 +1,187 @@ +import { createContext, useContext, useEffect, useState, ReactNode } from 'react' +import { supabase } from './supabase' +import type { Session, User, AuthError } from '@supabase/supabase-js' + +interface AuthContextType { + session: Session | null + user: User | null + loading: boolean + error: AuthError | null + signOut: () => Promise + refreshSession: () => Promise +} + +const AuthContext = createContext({ + session: null, + user: null, + loading: true, + error: null, + signOut: async () => {}, + refreshSession: async () => {} +}) + +export function AuthProvider({ children }: { children: ReactNode }) { + const [session, setSession] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const refreshSession = async () => { + try { + setLoading(true) + setError(null) + const { data, error } = await supabase.auth.refreshSession() + + if (error) { + console.error('[Auth Debug] Session refresh error:', error) + setError(error) + setSession(null) + } else { + console.log('[Auth Debug] Session refreshed successfully') + setSession(data.session) + } + } catch (err) { + console.error('[Auth Debug] Unexpected error during session refresh:', err) + setError(err as AuthError) + } finally { + setLoading(false) + } + } + + const signOut = async () => { + try { + setError(null) + const { error } = await supabase.auth.signOut() + + if (error) { + console.error('[Auth Debug] Sign out error:', error) + setError(error) + } else { + console.log('[Auth Debug] Signed out successfully') + setSession(null) + } + } catch (err) { + console.error('[Auth Debug] Unexpected error during sign out:', err) + setError(err as AuthError) + } + } + + useEffect(() => { + let mounted = true + + // Load current session on first mount + const initializeAuth = async () => { + try { + console.log('[Auth Debug] Initializing auth...') + const { data, error } = await supabase.auth.getSession() + + if (!mounted) return + + if (error) { + console.error('[Auth Debug] Initial session error:', error) + setError(error) + } else { + console.log('[Auth Debug] Initial session loaded:', { + hasSession: !!data.session, + userId: data.session?.user?.id, + email: data.session?.user?.email + }) + setSession(data.session) + } + } catch (err) { + console.error('[Auth Debug] Unexpected error during auth initialization:', err) + if (mounted) { + setError(err as AuthError) + } + } finally { + if (mounted) { + setLoading(false) + } + } + } + + initializeAuth() + + // Subscribe to auth state changes + const { data: { subscription } } = supabase.auth.onAuthStateChange( + async (event, newSession) => { + if (!mounted) return + + console.log('[Auth Debug] Auth state change:', { + event, + hasSession: !!newSession, + userId: newSession?.user?.id, + email: newSession?.user?.email, + timestamp: new Date().toISOString() + }) + + // Don't run supabase calls inside this callback; schedule them + setTimeout(() => { + if (mounted) { + setSession(newSession) + setError(null) + + // Additional handling for specific events + if (event === 'SIGNED_OUT') { + console.log('[Auth Debug] User signed out, clearing session') + } else if (event === 'SIGNED_IN') { + console.log('[Auth Debug] User signed in successfully') + } else if (event === 'TOKEN_REFRESHED') { + console.log('[Auth Debug] Token refreshed') + } else if (event === 'USER_UPDATED') { + console.log('[Auth Debug] User updated') + } + } + }, 0) + } + ) + + return () => { + mounted = false + subscription.unsubscribe() + } + }, []) + + const value: AuthContextType = { + session, + user: session?.user ?? null, + loading, + error, + signOut, + refreshSession + } + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider') + } + + return context +} + +// Debug hook to expose auth state for debugging +export function useAuthDebug() { + const auth = useAuth() + + useEffect(() => { + console.log('[Auth Debug] Current auth state:', { + hasSession: !!auth.session, + hasUser: !!auth.user, + loading: auth.loading, + hasError: !!auth.error, + userId: auth.user?.id, + email: auth.user?.email, + provider: auth.user?.app_metadata?.provider + }) + }, [auth.session, auth.user, auth.loading, auth.error]) + + return auth +} \ No newline at end of file diff --git a/frontend/src/routes/AuthCallback.tsx b/frontend/src/routes/AuthCallback.tsx new file mode 100644 index 000000000..ec5f87dff --- /dev/null +++ b/frontend/src/routes/AuthCallback.tsx @@ -0,0 +1,186 @@ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { supabase } from '../lib/supabase' + +interface CallbackState { + status: 'processing' | 'success' | 'error' + message: string + details?: Record +} + +export default function AuthCallback() { + const navigate = useNavigate() + const [state, setState] = useState({ + status: 'processing', + message: 'Processing authentication...' + }) + + useEffect(() => { + const handleCallback = async () => { + try { + const url = new URL(window.location.href) + const code = url.searchParams.get('code') + const error = url.searchParams.get('error') + const errorDescription = url.searchParams.get('error_description') + const next = url.searchParams.get('next') || '/' + + console.log('[Auth Callback Debug] URL parameters:', { + hasCode: !!code, + hasError: !!error, + error, + errorDescription, + next, + fullUrl: window.location.href + }) + + // Handle OAuth errors + if (error) { + const errorMsg = errorDescription || error + console.error('[Auth Callback Debug] OAuth error:', { error, errorDescription }) + + setState({ + status: 'error', + message: `Authentication failed: ${errorMsg}`, + details: { error, errorDescription } + }) + + // Redirect to login page after 3 seconds + setTimeout(() => navigate('/login', { replace: true }), 3000) + return + } + + // If PKCE/SSR-style code is present, exchange it for a session + if (code) { + console.log('[Auth Callback Debug] Exchanging code for session...') + + setState({ + status: 'processing', + message: 'Exchanging authorization code...' + }) + + const { data, error: exchangeError } = await supabase.auth.exchangeCodeForSession(code) + + if (exchangeError) { + console.error('[Auth Callback Debug] Code exchange error:', exchangeError) + + setState({ + status: 'error', + message: `Failed to complete sign in: ${exchangeError.message}`, + details: { exchangeError } + }) + + setTimeout(() => navigate('/login', { replace: true }), 3000) + return + } + + console.log('[Auth Callback Debug] Code exchange successful:', { + hasSession: !!data.session, + userId: data.session?.user?.id, + email: data.session?.user?.email + }) + + setState({ + status: 'success', + message: 'Sign in successful! Redirecting...', + details: { + userId: data.session?.user?.id, + email: data.session?.user?.email, + provider: data.session?.user?.app_metadata?.provider + } + }) + } else { + // No code present - might already be authenticated + console.log('[Auth Callback Debug] No code present, checking existing session...') + + const { data: sessionData } = await supabase.auth.getSession() + + if (sessionData.session) { + console.log('[Auth Callback Debug] Existing session found') + setState({ + status: 'success', + message: 'Already signed in! Redirecting...' + }) + } else { + console.log('[Auth Callback Debug] No session found') + setState({ + status: 'error', + message: 'No authentication data found' + }) + setTimeout(() => navigate('/login', { replace: true }), 2000) + return + } + } + + // Redirect to the intended destination + const destination = next.startsWith('/') ? next : '/' + console.log('[Auth Callback Debug] Redirecting to:', destination) + + setTimeout(() => navigate(destination, { replace: true }), 1500) + + } catch (err) { + console.error('[Auth Callback Debug] Unexpected error:', err) + + setState({ + status: 'error', + message: `Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`, + details: { error: err } + }) + + setTimeout(() => navigate('/login', { replace: true }), 3000) + } + } + + handleCallback() + }, [navigate]) + + const getStatusColor = () => { + switch (state.status) { + case 'processing': return 'text-blue-600' + case 'success': return 'text-green-600' + case 'error': return 'text-red-600' + default: return 'text-gray-600' + } + } + + const getStatusIcon = () => { + switch (state.status) { + case 'processing': return '🔄' + case 'success': return '✅' + case 'error': return '❌' + default: return '⏳' + } + } + + return ( +
+
+
+
{getStatusIcon()}
+

+ Authentication +

+

+ {state.message} +

+ + {import.meta.env.DEV && state.details && ( +
+ + Debug Information + +
+                {JSON.stringify(state.details, null, 2)}
+              
+
+ )} + + {state.status === 'processing' && ( +
+
+
+ )} +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/routes/AuthDebug.tsx b/frontend/src/routes/AuthDebug.tsx new file mode 100644 index 000000000..42beba9e1 --- /dev/null +++ b/frontend/src/routes/AuthDebug.tsx @@ -0,0 +1,477 @@ +import { useState } from 'react' +import { useAuth } from '../lib/useSession' +import { supabase } from '../lib/supabase' + +export default function AuthDebug() { + const { session, user, loading, error, signOut, refreshSession } = useAuth() + const [testResults, setTestResults] = useState(null) + const [isTestingAuth, setIsTestingAuth] = useState(false) + + // JWT API request state + const [apiUrl, setApiUrl] = useState(`${window.location.origin}/api/v1/admin/settings`) + const [apiMethod, setApiMethod] = useState<'GET' | 'POST' | 'PUT' | 'DELETE'>('GET') + const [apiRequestBody, setApiRequestBody] = useState('') + const [apiResponse, setApiResponse] = useState(null) + const [isTestingApi, setIsTestingApi] = useState(false) + + const runAuthTests = async () => { + setIsTestingAuth(true) + setTestResults(null) + + const results: any = { + timestamp: new Date().toISOString(), + tests: {} + } + + try { + // Test 1: Get current session + console.log('[Auth Debug] Testing current session...') + const { data: sessionData, error: sessionError } = await supabase.auth.getSession() + results.tests.currentSession = { + success: !sessionError, + hasSession: !!sessionData.session, + error: sessionError?.message, + userId: sessionData.session?.user?.id, + email: sessionData.session?.user?.email + } + + // Test 2: Get current user + console.log('[Auth Debug] Testing current user...') + const { data: userData, error: userError } = await supabase.auth.getUser() + results.tests.currentUser = { + success: !userError, + hasUser: !!userData.user, + error: userError?.message, + userId: userData.user?.id, + email: userData.user?.email + } + + // Test 3: Environment variables + results.tests.environment = { + supabaseUrl: import.meta.env.VITE_SUPABASE_URL || 'MISSING', + supabaseKey: import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY ? 'CONFIGURED' : 'MISSING', + mode: import.meta.env.MODE, + dev: import.meta.env.DEV + } + + // Test 4: Local storage + results.tests.localStorage = { + hasSupabaseSession: !!localStorage.getItem('sb-nrlkjfznsavsbmweiyqu-auth-token'), + keys: Object.keys(localStorage).filter(key => key.includes('supabase') || key.includes('sb-')) + } + + // Test 5: Context state + results.tests.contextState = { + hasSession: !!session, + hasUser: !!user, + loading, + hasError: !!error, + errorMessage: error?.message + } + + } catch (err) { + results.tests.unexpectedError = { + message: err instanceof Error ? err.message : 'Unknown error', + error: err + } + } + + console.log('[Auth Debug] Test results:', results) + setTestResults(results) + setIsTestingAuth(false) + } + + const clearLocalStorage = () => { + const keys = Object.keys(localStorage).filter(key => + key.includes('supabase') || key.includes('sb-') + ) + + keys.forEach(key => localStorage.removeItem(key)) + + console.log('[Auth Debug] Cleared local storage keys:', keys) + alert(`Cleared ${keys.length} auth-related localStorage keys`) + } + + const testSignIn = async () => { + try { + const redirectTo = `${window.location.origin}/auth/callback` + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { redirectTo } + }) + + if (error) { + alert(`Sign in test failed: ${error.message}`) + } + } catch (err) { + alert(`Sign in test error: ${err instanceof Error ? err.message : 'Unknown error'}`) + } + } + + const testApiRequest = async () => { + if (!session?.access_token) { + setApiResponse({ + error: 'No JWT token available. Please sign in first.', + timestamp: new Date().toISOString() + }) + return + } + + setIsTestingApi(true) + setApiResponse(null) + + const requestData = { + url: apiUrl, + method: apiMethod, + timestamp: new Date().toISOString(), + jwt: session.access_token.substring(0, 20) + '...' // Show partial token for debug + } + + try { + console.log('[API Debug] Making request with JWT:', requestData) + + const requestOptions: RequestInit = { + method: apiMethod, + headers: { + 'Authorization': `Bearer ${session.access_token}`, + 'Content-Type': 'application/json', + } + } + + // Add request body for POST/PUT requests + if ((apiMethod === 'POST' || apiMethod === 'PUT') && apiRequestBody.trim()) { + try { + JSON.parse(apiRequestBody) // Validate JSON + requestOptions.body = apiRequestBody + } catch (e) { + setApiResponse({ + error: 'Invalid JSON in request body', + timestamp: new Date().toISOString(), + requestData + }) + return + } + } + + const response = await fetch(apiUrl, requestOptions) + + let responseData: any + const contentType = response.headers.get('content-type') + + if (contentType && contentType.includes('application/json')) { + responseData = await response.json() + } else { + responseData = await response.text() + } + + const result = { + success: response.ok, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + data: responseData, + requestData, + timestamp: new Date().toISOString() + } + + console.log('[API Debug] Response:', result) + setApiResponse(result) + + } catch (err) { + const errorResult = { + error: err instanceof Error ? err.message : 'Unknown error', + requestData, + timestamp: new Date().toISOString() + } + + console.error('[API Debug] Request failed:', errorResult) + setApiResponse(errorResult) + } finally { + setIsTestingApi(false) + } + } + + return ( +
+
+ + {/* Header */} +
+

+ Authentication Debug Panel +

+

+ Debug and test authentication functionality +

+
+ + {/* Current Auth State */} +
+

+ Current Authentication State +

+ +
+
+
Loading
+
+ {loading ? 'Yes' : 'No'} +
+
+ +
+
Has Session
+
+ {session ? 'Yes' : 'No'} +
+
+ +
+
User ID
+
+ {user?.id || 'None'} +
+
+ +
+
Email
+
+ {user?.email || 'None'} +
+
+
+ + {error && ( +
+
Authentication Error
+
{error.message}
+
+ )} + + {session && ( +
+ + Full Session Data + +
+                {JSON.stringify(session, null, 2)}
+              
+
+ )} +
+ + {/* Actions */} +
+

Actions

+ +
+ + + + + + + {session && ( + + )} + + +
+
+ + {/* JWT API Request Testing */} + {session && ( +
+

+ JWT API Request Testing +

+

+ Test authenticated requests to your backend using the JWT token +

+ +
+ {/* URL Input */} +
+ + setApiUrl(e.target.value)} + placeholder="https://example.com/api/v1/admin/settings" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {/* Method Selection */} +
+ + +
+ + {/* Request Body (for POST/PUT) */} + {(apiMethod === 'POST' || apiMethod === 'PUT') && ( +
+ +