loginTest

This commit is contained in:
Anthony Stirling 2025-08-15 12:33:49 +01:00
parent 129e4d00e9
commit db815802f7
11 changed files with 1661 additions and 9 deletions

View File

@ -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"

View File

@ -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",

View File

@ -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 (
<RainbowThemeProvider>
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<FilesModalProvider>
<HomePage />
</FilesModalProvider>
</FileContextProvider>
<AuthProvider>
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<FilesModalProvider>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<Login />} />
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/debug" element={<AuthDebug />} />
{/* Catch-all route - redirects unknown paths to home */}
<Route path="*" element={<HomePage />} />
</Routes>
</FilesModalProvider>
</FileContextProvider>
</AuthProvider>
</RainbowThemeProvider>
);
}

View File

@ -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 (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4">
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between items-start mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Welcome back!</p>
</div>
<button
onClick={signOut}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Sign Out
</button>
</div>
<div className="space-y-4">
<div className="p-4 bg-green-50 border border-green-200 rounded-md">
<h3 className="font-medium text-green-900">Authentication Successful!</h3>
<p className="text-green-700 text-sm">You are signed in as {user?.email}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 bg-gray-50 rounded">
<div className="text-sm font-medium text-gray-700">User ID</div>
<div className="font-mono text-gray-900 break-all">{user?.id}</div>
</div>
<div className="p-4 bg-gray-50 rounded">
<div className="text-sm font-medium text-gray-700">Email</div>
<div className="text-gray-900">{user?.email}</div>
</div>
<div className="p-4 bg-gray-50 rounded">
<div className="text-sm font-medium text-gray-700">Provider</div>
<div className="text-gray-900">{user?.app_metadata?.provider}</div>
</div>
</div>
<details className="mt-4">
<summary className="cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
Full Session Data
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-48">
{JSON.stringify(session, null, 2)}
</pre>
</details>
</div>
</div>
</div>
</div>
)
}
// Example home page
function HomePage() {
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4 text-center">
<div className="bg-white rounded-lg shadow-md p-6">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
Stirling PDF - Authentication Demo
</h1>
<p className="text-gray-600 mb-6">
This is a demo of the Supabase authentication integration
</p>
<div className="space-x-4">
<a
href="/login"
className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Go to Login
</a>
<a
href="/dashboard"
className="inline-block px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
Protected Dashboard
</a>
<a
href="/debug"
className="inline-block px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Debug Panel
</a>
</div>
</div>
</div>
</div>
)
}
// Router configuration
const router = createBrowserRouter([
// Public routes
{ path: '/', element: <HomePage /> },
{ path: '/login', element: <Login /> },
{ path: '/auth/callback', element: <AuthCallback /> },
{ path: '/debug', element: <AuthDebug /> },
// Protected routes
{
path: '/dashboard',
element: (
<RequireAuth>
<ProtectedDashboard />
</RequireAuth>
)
},
])
// Main App component with auth provider
export default function AuthExample() {
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
)
}
// 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'

View File

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Checking authentication...</p>
</div>
</div>
)
}
// 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 <Navigate to={redirectPath} replace />
}
// Render protected content
return <>{children}</>
}
export default RequireAuth

View File

@ -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()
}

View File

@ -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<void>
refreshSession: () => Promise<void>
}
const AuthContext = createContext<AuthContextType>({
session: null,
user: null,
loading: true,
error: null,
signOut: async () => {},
refreshSession: async () => {}
})
export function AuthProvider({ children }: { children: ReactNode }) {
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<AuthError | null>(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 (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
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
}

View File

@ -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<string, any>
}
export default function AuthCallback() {
const navigate = useNavigate()
const [state, setState] = useState<CallbackState>({
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 (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
<div className="text-center">
<div className="text-4xl mb-4">{getStatusIcon()}</div>
<h1 className="text-2xl font-bold text-gray-900 mb-4">
Authentication
</h1>
<p className={`text-lg ${getStatusColor()}`}>
{state.message}
</p>
{import.meta.env.DEV && state.details && (
<details className="mt-6 text-left">
<summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
Debug Information
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto">
{JSON.stringify(state.details, null, 2)}
</pre>
</details>
)}
{state.status === 'processing' && (
<div className="mt-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
</div>
)}
</div>
</div>
</div>
)
}

View File

@ -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<any>(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<any>(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 (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4 space-y-8">
{/* Header */}
<div className="bg-white rounded-lg shadow-md p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-2">
Authentication Debug Panel
</h1>
<p className="text-gray-600">
Debug and test authentication functionality
</p>
</div>
{/* Current Auth State */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Current Authentication State
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="p-3 bg-gray-50 rounded">
<div className="text-sm font-medium text-gray-700">Loading</div>
<div className={`text-lg ${loading ? 'text-yellow-600' : 'text-green-600'}`}>
{loading ? 'Yes' : 'No'}
</div>
</div>
<div className="p-3 bg-gray-50 rounded">
<div className="text-sm font-medium text-gray-700">Has Session</div>
<div className={`text-lg ${session ? 'text-green-600' : 'text-red-600'}`}>
{session ? 'Yes' : 'No'}
</div>
</div>
<div className="p-3 bg-gray-50 rounded">
<div className="text-sm font-medium text-gray-700">User ID</div>
<div className="text-lg font-mono text-gray-900">
{user?.id || 'None'}
</div>
</div>
<div className="p-3 bg-gray-50 rounded">
<div className="text-sm font-medium text-gray-700">Email</div>
<div className="text-lg text-gray-900">
{user?.email || 'None'}
</div>
</div>
</div>
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-md mb-4">
<div className="text-red-800 text-sm font-medium">Authentication Error</div>
<div className="text-red-700 text-sm">{error.message}</div>
</div>
)}
{session && (
<details className="mb-4">
<summary className="cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
Full Session Data
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-48">
{JSON.stringify(session, null, 2)}
</pre>
</details>
)}
</div>
{/* Actions */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Actions</h2>
<div className="flex flex-wrap gap-3">
<button
onClick={runAuthTests}
disabled={isTestingAuth}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isTestingAuth ? 'Running Tests...' : 'Run Auth Tests'}
</button>
<button
onClick={refreshSession}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Refresh Session
</button>
<button
onClick={testSignIn}
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700"
>
Test Sign In
</button>
{session && (
<button
onClick={signOut}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Sign Out
</button>
)}
<button
onClick={clearLocalStorage}
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
>
Clear Local Storage
</button>
</div>
</div>
{/* JWT API Request Testing */}
{session && (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
JWT API Request Testing
</h2>
<p className="text-gray-600 mb-4">
Test authenticated requests to your backend using the JWT token
</p>
<div className="space-y-4">
{/* URL Input */}
<div>
<label htmlFor="api-url" className="block text-sm font-medium text-gray-700 mb-2">
API URL
</label>
<input
id="api-url"
type="url"
value={apiUrl}
onChange={(e) => 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"
/>
</div>
{/* Method Selection */}
<div>
<label htmlFor="api-method" className="block text-sm font-medium text-gray-700 mb-2">
HTTP Method
</label>
<select
id="api-method"
value={apiMethod}
onChange={(e) => setApiMethod(e.target.value as 'GET' | 'POST' | 'PUT' | 'DELETE')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
</select>
</div>
{/* Request Body (for POST/PUT) */}
{(apiMethod === 'POST' || apiMethod === 'PUT') && (
<div>
<label htmlFor="api-body" className="block text-sm font-medium text-gray-700 mb-2">
Request Body (JSON)
</label>
<textarea
id="api-body"
value={apiRequestBody}
onChange={(e) => setApiRequestBody(e.target.value)}
placeholder='{"key": "value"}'
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
/>
</div>
)}
{/* Token Info */}
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
<div className="text-sm font-medium text-blue-900 mb-1">JWT Token Info</div>
<div className="text-blue-700 text-sm space-y-1">
<div>Token: {session?.access_token ? `${session.access_token.substring(0, 30)}...` : 'No token'}</div>
<div>Expires: {session?.expires_at ? new Date(session.expires_at * 1000).toLocaleString() : 'Unknown'}</div>
</div>
</div>
{/* Send Request Button */}
<button
onClick={testApiRequest}
disabled={isTestingApi || !apiUrl}
className="w-full px-4 py-3 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{isTestingApi ? 'Sending Request...' : `Send ${apiMethod} Request`}
</button>
</div>
</div>
)}
{/* API Response */}
{apiResponse && (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">API Response</h2>
{apiResponse.error ? (
<div className="p-4 bg-red-50 border border-red-200 rounded-md mb-4">
<div className="text-red-800 text-sm font-medium">Request Failed</div>
<div className="text-red-700 text-sm">{apiResponse.error}</div>
</div>
) : (
<div className={`p-4 ${apiResponse.success ? 'bg-green-50 border-green-200' : 'bg-yellow-50 border-yellow-200'} border rounded-md mb-4`}>
<div className={`text-sm font-medium ${apiResponse.success ? 'text-green-800' : 'text-yellow-800'}`}>
{apiResponse.status} {apiResponse.statusText}
</div>
<div className={`text-sm ${apiResponse.success ? 'text-green-700' : 'text-yellow-700'}`}>
Request {apiResponse.success ? 'successful' : 'completed with non-2xx status'}
</div>
</div>
)}
<div className="space-y-4">
{/* Response Headers */}
{apiResponse.headers && (
<details>
<summary className="cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
Response Headers
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-32">
{JSON.stringify(apiResponse.headers, null, 2)}
</pre>
</details>
)}
{/* Response Data */}
<details open>
<summary className="cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
Response Data
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-96">
{typeof apiResponse.data === 'string'
? apiResponse.data
: JSON.stringify(apiResponse.data, null, 2)}
</pre>
</details>
{/* Request Debug Info */}
{apiResponse.requestData && (
<details>
<summary className="cursor-pointer text-sm font-medium text-gray-700 hover:text-gray-900">
Request Debug Info
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-32">
{JSON.stringify(apiResponse.requestData, null, 2)}
</pre>
</details>
)}
</div>
</div>
)}
{/* Test Results */}
{testResults && (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Test Results</h2>
<pre className="p-4 bg-gray-100 rounded text-sm overflow-auto max-h-96">
{JSON.stringify(testResults, null, 2)}
</pre>
</div>
)}
{/* Environment Info */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Environment</h2>
<div className="space-y-2 text-sm">
<div><strong>Mode:</strong> {import.meta.env.MODE}</div>
<div><strong>Dev:</strong> {import.meta.env.DEV ? 'Yes' : 'No'}</div>
<div><strong>Supabase URL:</strong> {import.meta.env.VITE_SUPABASE_URL || 'NOT SET'}</div>
<div><strong>Supabase Key:</strong> {import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY ? 'CONFIGURED' : 'NOT SET'}</div>
<div><strong>Origin:</strong> {window.location.origin}</div>
<div><strong>Callback URL:</strong> {window.location.origin}/auth/callback</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,317 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { supabase } from '../lib/supabase'
import { useAuth } from '../lib/useSession'
export default function Login() {
const navigate = useNavigate()
const { session, user, loading, signOut } = useAuth()
const [isSigningIn, setIsSigningIn] = useState(false)
const [error, setError] = useState<string | null>(null)
const [debugInfo, setDebugInfo] = useState<any>(null)
// Show logged in state instead of redirecting
if (session && !loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-2xl w-full bg-white rounded-lg shadow-md p-8">
<div className="text-center mb-8">
<div className="text-6xl mb-4"></div>
<h1 className="text-3xl font-bold text-green-600 mb-2">
YOU ARE LOGGED IN
</h1>
<p className="text-gray-600">
Successfully authenticated with Supabase
</p>
</div>
<div className="space-y-6">
{/* User Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
<div className="text-sm font-medium text-blue-900 mb-1">User ID</div>
<div className="font-mono text-blue-800 break-all text-sm">
{user?.id}
</div>
</div>
<div className="p-4 bg-green-50 border border-green-200 rounded-md">
<div className="text-sm font-medium text-green-900 mb-1">Email</div>
<div className="text-green-800">
{user?.email}
</div>
</div>
<div className="p-4 bg-purple-50 border border-purple-200 rounded-md">
<div className="text-sm font-medium text-purple-900 mb-1">Provider</div>
<div className="text-purple-800">
{user?.app_metadata?.provider || 'Unknown'}
</div>
</div>
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<div className="text-sm font-medium text-yellow-900 mb-1">Created</div>
<div className="text-yellow-800 text-sm">
{user?.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'}
</div>
</div>
</div>
{/* JWT Token Display */}
<div className="p-4 bg-gray-50 border border-gray-200 rounded-md">
<div className="text-sm font-medium text-gray-900 mb-2">JWT Access Token</div>
<div className="font-mono text-xs bg-white p-3 rounded border break-all text-gray-800">
{session?.access_token}
</div>
<div className="mt-2 text-xs text-gray-600">
<strong>Expires:</strong> {session?.expires_at ? new Date(session.expires_at * 1000).toLocaleString() : 'Unknown'}
</div>
</div>
{/* Refresh Token (if available) */}
{session?.refresh_token && (
<div className="p-4 bg-gray-50 border border-gray-200 rounded-md">
<div className="text-sm font-medium text-gray-900 mb-2">Refresh Token</div>
<div className="font-mono text-xs bg-white p-3 rounded border break-all text-gray-800">
{session.refresh_token}
</div>
</div>
)}
{/* User Metadata */}
{(user?.user_metadata && Object.keys(user.user_metadata).length > 0) && (
<details className="p-4 bg-indigo-50 border border-indigo-200 rounded-md">
<summary className="cursor-pointer text-sm font-medium text-indigo-900 hover:text-indigo-700">
User Metadata
</summary>
<pre className="mt-2 p-3 bg-white rounded text-xs overflow-auto max-h-32 text-gray-800">
{JSON.stringify(user.user_metadata, null, 2)}
</pre>
</details>
)}
{/* App Metadata */}
{(user?.app_metadata && Object.keys(user.app_metadata).length > 0) && (
<details className="p-4 bg-orange-50 border border-orange-200 rounded-md">
<summary className="cursor-pointer text-sm font-medium text-orange-900 hover:text-orange-700">
App Metadata
</summary>
<pre className="mt-2 p-3 bg-white rounded text-xs overflow-auto max-h-32 text-gray-800">
{JSON.stringify(user.app_metadata, null, 2)}
</pre>
</details>
)}
{/* Full Session Data */}
<details className="p-4 bg-red-50 border border-red-200 rounded-md">
<summary className="cursor-pointer text-sm font-medium text-red-900 hover:text-red-700">
Full Session Object
</summary>
<pre className="mt-2 p-3 bg-white rounded text-xs overflow-auto max-h-48 text-gray-800">
{JSON.stringify(session, null, 2)}
</pre>
</details>
{/* Action Buttons */}
<div className="flex flex-wrap gap-3 pt-4 border-t">
<button
onClick={() => navigate('/')}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Go to Home
</button>
<button
onClick={() => navigate('/debug')}
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700"
>
Debug Panel
</button>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Refresh Session
</button>
<button
onClick={async () => {
await signOut()
window.location.reload()
}}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Sign Out
</button>
</div>
</div>
</div>
</div>
)
}
const signInWithGitHub = async (nextPath = '/') => {
try {
setIsSigningIn(true)
setError(null)
setDebugInfo(null)
const redirectTo = `${window.location.origin}/auth/callback?next=${encodeURIComponent(nextPath)}`
console.log('[Login Debug] Initiating GitHub OAuth:', {
redirectTo,
nextPath,
origin: window.location.origin
})
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo,
queryParams: {
access_type: 'offline',
prompt: 'consent',
}
}
})
console.log('[Login Debug] OAuth response:', { data, error })
if (error) {
console.error('[Login Debug] OAuth initiation error:', error)
setError(`Failed to initiate sign in: ${error.message}`)
setDebugInfo({ error })
} else {
console.log('[Login Debug] OAuth initiated successfully, redirecting...')
// OAuth redirect should happen automatically
// If we reach here without redirect, there might be an issue
setTimeout(() => {
if (!window.location.href.includes('github.com')) {
setError('OAuth redirect did not occur as expected')
setDebugInfo({
message: 'Expected redirect to GitHub but still on our domain',
currentUrl: window.location.href
})
}
}, 2000)
}
} catch (err) {
console.error('[Login Debug] Unexpected error:', err)
setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`)
setDebugInfo({ error: err })
} finally {
setIsSigningIn(false)
}
}
const testSupabaseConnection = async () => {
try {
console.log('[Login Debug] Testing Supabase connection...')
setError(null)
// Test basic connection
const { data, error } = await supabase.auth.getSession()
const testResult = {
connectionSuccess: !error,
hasSession: !!data.session,
error: error?.message,
url: supabase.supabaseUrl,
key: supabase.supabaseKey.substring(0, 20) + '...'
}
console.log('[Login Debug] Connection test result:', testResult)
setDebugInfo(testResult)
if (error) {
setError(`Connection test failed: ${error.message}`)
}
} catch (err) {
console.error('[Login Debug] Connection test error:', err)
setError(`Connection test error: ${err instanceof Error ? err.message : 'Unknown error'}`)
}
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Welcome to Stirling PDF
</h1>
<p className="text-gray-600">
Sign in to access your account
</p>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800 text-sm font-medium">Error</p>
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
<div className="space-y-4">
<button
onClick={() => signInWithGitHub()}
disabled={isSigningIn}
className="w-full flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
{isSigningIn ? 'Signing in...' : 'Continue with GitHub'}
</button>
{import.meta.env.DEV && (
<div className="border-t pt-4 space-y-3">
<h3 className="text-sm font-medium text-gray-700">Development Tools</h3>
<button
onClick={testSupabaseConnection}
className="w-full px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Test Supabase Connection
</button>
<div className="text-xs text-gray-500 space-y-1">
<p><strong>Environment:</strong> {import.meta.env.MODE}</p>
<p><strong>Supabase URL:</strong> {import.meta.env.VITE_SUPABASE_URL ? '✓ Configured' : '✗ Missing'}</p>
<p><strong>Supabase Key:</strong> {import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY ? '✓ Configured' : '✗ Missing'}</p>
</div>
</div>
)}
</div>
{debugInfo && import.meta.env.DEV && (
<details className="mt-6">
<summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
Debug Information
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-48">
{JSON.stringify(debugInfo, null, 2)}
</pre>
</details>
)}
<div className="mt-8 text-center">
<p className="text-xs text-gray-500">
This is a demo login page for testing authentication
</p>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,87 @@
import { useState } from 'react'
import { useAuth } from '../lib/useSession'
// Simplified login page for testing without redirect logic
export default function LoginTest() {
const { session, loading, error } = useAuth()
const [debugInfo, setDebugInfo] = useState<any>(null)
console.log('[LoginTest Debug] Component rendered:', {
hasSession: !!session,
loading,
hasError: !!error,
timestamp: new Date().toISOString()
})
const testConnection = () => {
const info = {
authState: {
hasSession: !!session,
loading,
hasError: !!error,
errorMessage: error?.message
},
environment: {
supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
hasKey: !!import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY,
mode: import.meta.env.MODE
},
location: {
href: window.location.href,
pathname: window.location.pathname,
origin: window.location.origin
}
}
console.log('[LoginTest Debug] Connection test:', info)
setDebugInfo(info)
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white rounded-lg shadow-md p-8">
<h1 className="text-2xl font-bold text-gray-900 mb-4 text-center">
Login Test Page
</h1>
<div className="space-y-4">
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
<h3 className="font-medium text-blue-900 mb-2">Auth Status</h3>
<div className="text-sm text-blue-800 space-y-1">
<div>Loading: {loading ? 'Yes' : 'No'}</div>
<div>Has Session: {session ? 'Yes' : 'No'}</div>
<div>Error: {error ? error.message : 'None'}</div>
</div>
</div>
<button
onClick={testConnection}
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Test Connection
</button>
{debugInfo && (
<details className="mt-4">
<summary className="cursor-pointer text-sm font-medium text-gray-700">
Debug Info
</summary>
<pre className="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-48">
{JSON.stringify(debugInfo, null, 2)}
</pre>
</details>
)}
<div className="text-center space-y-2">
<p className="text-sm text-gray-600">
If you can see this page, routing is working
</p>
<p className="text-xs text-gray-500">
Path: {window.location.pathname}
</p>
</div>
</div>
</div>
</div>
)
}