mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
loginTest
This commit is contained in:
parent
129e4d00e9
commit
db815802f7
117
frontend/package-lock.json
generated
117
frontend/package-lock.json
generated
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
171
frontend/src/AuthExample.tsx
Normal file
171
frontend/src/AuthExample.tsx
Normal 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'
|
45
frontend/src/components/auth/RequireAuth.tsx
Normal file
45
frontend/src/components/auth/RequireAuth.tsx
Normal 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
|
58
frontend/src/lib/supabase.ts
Normal file
58
frontend/src/lib/supabase.ts
Normal 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()
|
||||
}
|
187
frontend/src/lib/useSession.tsx
Normal file
187
frontend/src/lib/useSession.tsx
Normal 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
|
||||
}
|
186
frontend/src/routes/AuthCallback.tsx
Normal file
186
frontend/src/routes/AuthCallback.tsx
Normal 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>
|
||||
)
|
||||
}
|
477
frontend/src/routes/AuthDebug.tsx
Normal file
477
frontend/src/routes/AuthDebug.tsx
Normal 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>
|
||||
)
|
||||
}
|
317
frontend/src/routes/Login.tsx
Normal file
317
frontend/src/routes/Login.tsx
Normal 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>
|
||||
)
|
||||
}
|
87
frontend/src/routes/LoginTest.tsx
Normal file
87
frontend/src/routes/LoginTest.tsx
Normal 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>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user