diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b2141beb7..7c29e9e65 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -16,6 +16,7 @@
"@mantine/hooks": "^8.0.1",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
+ "@supabase/supabase-js": "^2.55.0",
"@tailwindcss/postcss": "^4.1.8",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -1918,6 +1919,102 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.71.1",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz",
+ "integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.5.tgz",
+ "integrity": "sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/node-fetch": {
+ "version": "2.6.15",
+ "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
+ "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ }
+ },
+ "node_modules/@supabase/node-fetch/node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/@supabase/node-fetch/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@supabase/node-fetch/node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "1.19.4",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
+ "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.1.tgz",
+ "integrity": "sha512-edRFa2IrQw50kNntvUyS38hsL7t2d/psah6om6aNTLLcWem0R6bOUq7sk7DsGeSlNfuwEwWn57FdYSva6VddYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.13",
+ "@types/phoenix": "^1.6.6",
+ "@types/ws": "^8.18.1",
+ "ws": "^8.18.2"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.11.0.tgz",
+ "integrity": "sha512-Y+kx/wDgd4oasAgoAq0bsbQojwQ+ejIif8uczZ9qufRHWFLMU5cODT+ApHsSrDufqUcVKt+eyxtOXSkeh2v9ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/node-fetch": "^2.6.14"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.55.0",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.55.0.tgz",
+ "integrity": "sha512-Y1uV4nEMjQV1x83DGn7+Z9LOisVVRlY1geSARrUHbXWgbyKLZ6/08dvc0Us1r6AJ4tcKpwpCZWG9yDQYo1JgHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.71.1",
+ "@supabase/functions-js": "2.4.5",
+ "@supabase/node-fetch": "2.6.15",
+ "@supabase/postgrest-js": "1.19.4",
+ "@supabase/realtime-js": "2.15.1",
+ "@supabase/storage-js": "^2.10.4"
+ }
+ },
"node_modules/@tailwindcss/node": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz",
@@ -2389,7 +2486,6 @@
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
- "dev": true,
"dependencies": {
"undici-types": "~7.10.0"
}
@@ -2400,6 +2496,12 @@
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
+ "node_modules/@types/phoenix": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
+ "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
+ "license": "MIT"
+ },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -2434,6 +2536,15 @@
"@types/react": "*"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz",
@@ -7417,8 +7528,7 @@
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
- "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
- "dev": true
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
},
"node_modules/universalify": {
"version": "2.0.1",
@@ -8899,7 +9009,6 @@
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/frontend/package.json b/frontend/package.json
index b59be58e9..bfcd83f6d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
"@mantine/hooks": "^8.0.1",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
+ "@supabase/supabase-js": "^2.55.0",
"@tailwindcss/postcss": "^4.1.8",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 852204b25..f6c36ca58 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,8 +1,13 @@
import React from 'react';
+import { Routes, Route } from 'react-router-dom';
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
import { FileContextProvider } from './contexts/FileContext';
import { FilesModalProvider } from './contexts/FilesModalContext';
+import { AuthProvider } from './lib/useSession';
import HomePage from './pages/HomePage';
+import Login from './routes/Login';
+import AuthCallback from './routes/AuthCallback';
+import AuthDebug from './routes/AuthDebug';
// Import global styles
import './styles/tailwind.css';
@@ -11,11 +16,20 @@ import './index.css';
export default function App() {
return (
-
-
-
-
-
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ {/* Catch-all route - redirects unknown paths to home */}
+ } />
+
+
+
+
);
}
diff --git a/frontend/src/AuthExample.tsx b/frontend/src/AuthExample.tsx
new file mode 100644
index 000000000..53fe04b90
--- /dev/null
+++ b/frontend/src/AuthExample.tsx
@@ -0,0 +1,171 @@
+import { createBrowserRouter, RouterProvider } from 'react-router-dom'
+import { AuthProvider, useAuth } from './lib/useSession'
+import { supabase } from './lib/supabase'
+import RequireAuth from './components/auth/RequireAuth'
+import Login from './routes/Login'
+import AuthCallback from './routes/AuthCallback'
+import AuthDebug from './routes/AuthDebug'
+
+// Example protected component
+function ProtectedDashboard() {
+ const { session, user, signOut } = useAuth()
+
+ return (
+
+
+
+
+
+
Dashboard
+
Welcome back!
+
+
+
+
+
+
+
Authentication Successful!
+
You are signed in as {user?.email}
+
+
+
+
+
+
Email
+
{user?.email}
+
+
+
Provider
+
{user?.app_metadata?.provider}
+
+
+
+
+
+ Full Session Data
+
+
+ {JSON.stringify(session, null, 2)}
+
+
+
+
+
+
+ )
+}
+
+// Example home page
+function HomePage() {
+ return (
+
+
+
+
+ Stirling PDF - Authentication Demo
+
+
+ This is a demo of the Supabase authentication integration
+
+
+
+
+
+
+ )
+}
+
+// Router configuration
+const router = createBrowserRouter([
+ // Public routes
+ { path: '/', element: },
+ { path: '/login', element: },
+ { path: '/auth/callback', element: },
+ { path: '/debug', element: },
+
+ // Protected routes
+ {
+ path: '/dashboard',
+ element: (
+
+
+
+ )
+ },
+])
+
+// Main App component with auth provider
+export default function AuthExample() {
+ return (
+
+
+
+ )
+}
+
+// Additional utility functions for easy integration
+export const authUtils = {
+ // Sign in with GitHub (can be called from anywhere)
+ signInWithGitHub: async (nextPath = '/') => {
+ const redirectTo = `${window.location.origin}/auth/callback?next=${encodeURIComponent(nextPath)}`
+ const { error } = await supabase.auth.signInWithOAuth({
+ provider: 'github',
+ options: { redirectTo }
+ })
+ if (error) {
+ console.error('Sign in error:', error)
+ throw error
+ }
+ },
+
+ // Sign out (can be called from anywhere)
+ signOut: async () => {
+ const { error } = await supabase.auth.signOut()
+ if (error) {
+ console.error('Sign out error:', error)
+ throw error
+ }
+ },
+
+ // Get current session
+ getCurrentSession: async () => {
+ const { data, error } = await supabase.auth.getSession()
+ return { session: data.session, error }
+ },
+
+ // Check if user is authenticated
+ isAuthenticated: async () => {
+ const { session } = await authUtils.getCurrentSession()
+ return !!session
+ }
+}
+
+// Import this in your main App.tsx or wherever you want to add auth
+// import AuthExample from './AuthExample'
\ No newline at end of file
diff --git a/frontend/src/components/auth/RequireAuth.tsx b/frontend/src/components/auth/RequireAuth.tsx
new file mode 100644
index 000000000..e5fddc2b3
--- /dev/null
+++ b/frontend/src/components/auth/RequireAuth.tsx
@@ -0,0 +1,45 @@
+import { ReactNode } from 'react'
+import { Navigate, useLocation } from 'react-router-dom'
+import { useAuth } from '../../lib/useSession'
+
+interface RequireAuthProps {
+ children: ReactNode
+ fallbackPath?: string
+}
+
+export function RequireAuth({ children, fallbackPath = '/login' }: RequireAuthProps) {
+ const { session, loading, error } = useAuth()
+ const location = useLocation()
+
+ console.log('[RequireAuth Debug] Auth check:', {
+ hasSession: !!session,
+ loading,
+ hasError: !!error,
+ currentPath: location.pathname,
+ fallbackPath
+ })
+
+ // Show loading spinner while checking auth
+ if (loading) {
+ return (
+
+
+
+
Checking authentication...
+
+
+ )
+ }
+
+ // Redirect to login if not authenticated
+ if (!session) {
+ const redirectPath = `${fallbackPath}?next=${encodeURIComponent(location.pathname + location.search)}`
+ console.log('[RequireAuth Debug] Redirecting to login:', redirectPath)
+ return
+ }
+
+ // Render protected content
+ return <>{children}>
+}
+
+export default RequireAuth
\ No newline at end of file
diff --git a/frontend/src/lib/supabase.ts b/frontend/src/lib/supabase.ts
new file mode 100644
index 000000000..9fe9c80ac
--- /dev/null
+++ b/frontend/src/lib/supabase.ts
@@ -0,0 +1,58 @@
+import { createClient } from '@supabase/supabase-js'
+
+// Debug helper to log Supabase configuration
+const debugConfig = () => {
+ const url = import.meta.env.VITE_SUPABASE_URL
+ const key = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY
+
+ console.log('[Supabase Debug] Configuration:', {
+ url: url ? '✓ URL configured' : '✗ URL missing',
+ key: key ? '✓ Key configured' : '✗ Key missing',
+ urlValue: url || 'undefined',
+ keyValue: key ? `${key.substring(0, 20)}...` : 'undefined'
+ })
+
+ return { url, key }
+}
+
+const config = debugConfig()
+
+if (!config.url) {
+ throw new Error('Missing VITE_SUPABASE_URL environment variable')
+}
+
+if (!config.key) {
+ throw new Error('Missing VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY environment variable')
+}
+
+export const supabase = createClient(
+ config.url,
+ config.key,
+ {
+ auth: {
+ persistSession: true, // keep session in localStorage
+ autoRefreshToken: true,
+ detectSessionInUrl: true, // helpful on first load after redirect
+ debug: import.meta.env.DEV, // Enable debug logs in development
+ },
+ }
+)
+
+// Debug helper for auth events
+export const debugAuthEvents = () => {
+ supabase.auth.onAuthStateChange((event, session) => {
+ console.log('[Supabase Debug] Auth state change:', {
+ event,
+ hasSession: !!session,
+ userId: session?.user?.id,
+ email: session?.user?.email,
+ provider: session?.user?.app_metadata?.provider,
+ timestamp: new Date().toISOString()
+ })
+ })
+}
+
+// Call this in development to enable auth debugging
+if (import.meta.env.DEV) {
+ debugAuthEvents()
+}
\ No newline at end of file
diff --git a/frontend/src/lib/useSession.tsx b/frontend/src/lib/useSession.tsx
new file mode 100644
index 000000000..e80a7f1e5
--- /dev/null
+++ b/frontend/src/lib/useSession.tsx
@@ -0,0 +1,187 @@
+import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
+import { supabase } from './supabase'
+import type { Session, User, AuthError } from '@supabase/supabase-js'
+
+interface AuthContextType {
+ session: Session | null
+ user: User | null
+ loading: boolean
+ error: AuthError | null
+ signOut: () => Promise
+ refreshSession: () => Promise
+}
+
+const AuthContext = createContext({
+ session: null,
+ user: null,
+ loading: true,
+ error: null,
+ signOut: async () => {},
+ refreshSession: async () => {}
+})
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [session, setSession] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ const refreshSession = async () => {
+ try {
+ setLoading(true)
+ setError(null)
+ const { data, error } = await supabase.auth.refreshSession()
+
+ if (error) {
+ console.error('[Auth Debug] Session refresh error:', error)
+ setError(error)
+ setSession(null)
+ } else {
+ console.log('[Auth Debug] Session refreshed successfully')
+ setSession(data.session)
+ }
+ } catch (err) {
+ console.error('[Auth Debug] Unexpected error during session refresh:', err)
+ setError(err as AuthError)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const signOut = async () => {
+ try {
+ setError(null)
+ const { error } = await supabase.auth.signOut()
+
+ if (error) {
+ console.error('[Auth Debug] Sign out error:', error)
+ setError(error)
+ } else {
+ console.log('[Auth Debug] Signed out successfully')
+ setSession(null)
+ }
+ } catch (err) {
+ console.error('[Auth Debug] Unexpected error during sign out:', err)
+ setError(err as AuthError)
+ }
+ }
+
+ useEffect(() => {
+ let mounted = true
+
+ // Load current session on first mount
+ const initializeAuth = async () => {
+ try {
+ console.log('[Auth Debug] Initializing auth...')
+ const { data, error } = await supabase.auth.getSession()
+
+ if (!mounted) return
+
+ if (error) {
+ console.error('[Auth Debug] Initial session error:', error)
+ setError(error)
+ } else {
+ console.log('[Auth Debug] Initial session loaded:', {
+ hasSession: !!data.session,
+ userId: data.session?.user?.id,
+ email: data.session?.user?.email
+ })
+ setSession(data.session)
+ }
+ } catch (err) {
+ console.error('[Auth Debug] Unexpected error during auth initialization:', err)
+ if (mounted) {
+ setError(err as AuthError)
+ }
+ } finally {
+ if (mounted) {
+ setLoading(false)
+ }
+ }
+ }
+
+ initializeAuth()
+
+ // Subscribe to auth state changes
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
+ async (event, newSession) => {
+ if (!mounted) return
+
+ console.log('[Auth Debug] Auth state change:', {
+ event,
+ hasSession: !!newSession,
+ userId: newSession?.user?.id,
+ email: newSession?.user?.email,
+ timestamp: new Date().toISOString()
+ })
+
+ // Don't run supabase calls inside this callback; schedule them
+ setTimeout(() => {
+ if (mounted) {
+ setSession(newSession)
+ setError(null)
+
+ // Additional handling for specific events
+ if (event === 'SIGNED_OUT') {
+ console.log('[Auth Debug] User signed out, clearing session')
+ } else if (event === 'SIGNED_IN') {
+ console.log('[Auth Debug] User signed in successfully')
+ } else if (event === 'TOKEN_REFRESHED') {
+ console.log('[Auth Debug] Token refreshed')
+ } else if (event === 'USER_UPDATED') {
+ console.log('[Auth Debug] User updated')
+ }
+ }
+ }, 0)
+ }
+ )
+
+ return () => {
+ mounted = false
+ subscription.unsubscribe()
+ }
+ }, [])
+
+ const value: AuthContextType = {
+ session,
+ user: session?.user ?? null,
+ loading,
+ error,
+ signOut,
+ refreshSession
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext)
+
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider')
+ }
+
+ return context
+}
+
+// Debug hook to expose auth state for debugging
+export function useAuthDebug() {
+ const auth = useAuth()
+
+ useEffect(() => {
+ console.log('[Auth Debug] Current auth state:', {
+ hasSession: !!auth.session,
+ hasUser: !!auth.user,
+ loading: auth.loading,
+ hasError: !!auth.error,
+ userId: auth.user?.id,
+ email: auth.user?.email,
+ provider: auth.user?.app_metadata?.provider
+ })
+ }, [auth.session, auth.user, auth.loading, auth.error])
+
+ return auth
+}
\ No newline at end of file
diff --git a/frontend/src/routes/AuthCallback.tsx b/frontend/src/routes/AuthCallback.tsx
new file mode 100644
index 000000000..ec5f87dff
--- /dev/null
+++ b/frontend/src/routes/AuthCallback.tsx
@@ -0,0 +1,186 @@
+import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { supabase } from '../lib/supabase'
+
+interface CallbackState {
+ status: 'processing' | 'success' | 'error'
+ message: string
+ details?: Record
+}
+
+export default function AuthCallback() {
+ const navigate = useNavigate()
+ const [state, setState] = useState({
+ status: 'processing',
+ message: 'Processing authentication...'
+ })
+
+ useEffect(() => {
+ const handleCallback = async () => {
+ try {
+ const url = new URL(window.location.href)
+ const code = url.searchParams.get('code')
+ const error = url.searchParams.get('error')
+ const errorDescription = url.searchParams.get('error_description')
+ const next = url.searchParams.get('next') || '/'
+
+ console.log('[Auth Callback Debug] URL parameters:', {
+ hasCode: !!code,
+ hasError: !!error,
+ error,
+ errorDescription,
+ next,
+ fullUrl: window.location.href
+ })
+
+ // Handle OAuth errors
+ if (error) {
+ const errorMsg = errorDescription || error
+ console.error('[Auth Callback Debug] OAuth error:', { error, errorDescription })
+
+ setState({
+ status: 'error',
+ message: `Authentication failed: ${errorMsg}`,
+ details: { error, errorDescription }
+ })
+
+ // Redirect to login page after 3 seconds
+ setTimeout(() => navigate('/login', { replace: true }), 3000)
+ return
+ }
+
+ // If PKCE/SSR-style code is present, exchange it for a session
+ if (code) {
+ console.log('[Auth Callback Debug] Exchanging code for session...')
+
+ setState({
+ status: 'processing',
+ message: 'Exchanging authorization code...'
+ })
+
+ const { data, error: exchangeError } = await supabase.auth.exchangeCodeForSession(code)
+
+ if (exchangeError) {
+ console.error('[Auth Callback Debug] Code exchange error:', exchangeError)
+
+ setState({
+ status: 'error',
+ message: `Failed to complete sign in: ${exchangeError.message}`,
+ details: { exchangeError }
+ })
+
+ setTimeout(() => navigate('/login', { replace: true }), 3000)
+ return
+ }
+
+ console.log('[Auth Callback Debug] Code exchange successful:', {
+ hasSession: !!data.session,
+ userId: data.session?.user?.id,
+ email: data.session?.user?.email
+ })
+
+ setState({
+ status: 'success',
+ message: 'Sign in successful! Redirecting...',
+ details: {
+ userId: data.session?.user?.id,
+ email: data.session?.user?.email,
+ provider: data.session?.user?.app_metadata?.provider
+ }
+ })
+ } else {
+ // No code present - might already be authenticated
+ console.log('[Auth Callback Debug] No code present, checking existing session...')
+
+ const { data: sessionData } = await supabase.auth.getSession()
+
+ if (sessionData.session) {
+ console.log('[Auth Callback Debug] Existing session found')
+ setState({
+ status: 'success',
+ message: 'Already signed in! Redirecting...'
+ })
+ } else {
+ console.log('[Auth Callback Debug] No session found')
+ setState({
+ status: 'error',
+ message: 'No authentication data found'
+ })
+ setTimeout(() => navigate('/login', { replace: true }), 2000)
+ return
+ }
+ }
+
+ // Redirect to the intended destination
+ const destination = next.startsWith('/') ? next : '/'
+ console.log('[Auth Callback Debug] Redirecting to:', destination)
+
+ setTimeout(() => navigate(destination, { replace: true }), 1500)
+
+ } catch (err) {
+ console.error('[Auth Callback Debug] Unexpected error:', err)
+
+ setState({
+ status: 'error',
+ message: `Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`,
+ details: { error: err }
+ })
+
+ setTimeout(() => navigate('/login', { replace: true }), 3000)
+ }
+ }
+
+ handleCallback()
+ }, [navigate])
+
+ const getStatusColor = () => {
+ switch (state.status) {
+ case 'processing': return 'text-blue-600'
+ case 'success': return 'text-green-600'
+ case 'error': return 'text-red-600'
+ default: return 'text-gray-600'
+ }
+ }
+
+ const getStatusIcon = () => {
+ switch (state.status) {
+ case 'processing': return '🔄'
+ case 'success': return '✅'
+ case 'error': return '❌'
+ default: return '⏳'
+ }
+ }
+
+ return (
+
+
+
+
{getStatusIcon()}
+
+ Authentication
+
+
+ {state.message}
+
+
+ {import.meta.env.DEV && state.details && (
+
+
+ Debug Information
+
+
+ {JSON.stringify(state.details, null, 2)}
+
+
+ )}
+
+ {state.status === 'processing' && (
+
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/routes/AuthDebug.tsx b/frontend/src/routes/AuthDebug.tsx
new file mode 100644
index 000000000..42beba9e1
--- /dev/null
+++ b/frontend/src/routes/AuthDebug.tsx
@@ -0,0 +1,477 @@
+import { useState } from 'react'
+import { useAuth } from '../lib/useSession'
+import { supabase } from '../lib/supabase'
+
+export default function AuthDebug() {
+ const { session, user, loading, error, signOut, refreshSession } = useAuth()
+ const [testResults, setTestResults] = useState(null)
+ const [isTestingAuth, setIsTestingAuth] = useState(false)
+
+ // JWT API request state
+ const [apiUrl, setApiUrl] = useState(`${window.location.origin}/api/v1/admin/settings`)
+ const [apiMethod, setApiMethod] = useState<'GET' | 'POST' | 'PUT' | 'DELETE'>('GET')
+ const [apiRequestBody, setApiRequestBody] = useState('')
+ const [apiResponse, setApiResponse] = useState(null)
+ const [isTestingApi, setIsTestingApi] = useState(false)
+
+ const runAuthTests = async () => {
+ setIsTestingAuth(true)
+ setTestResults(null)
+
+ const results: any = {
+ timestamp: new Date().toISOString(),
+ tests: {}
+ }
+
+ try {
+ // Test 1: Get current session
+ console.log('[Auth Debug] Testing current session...')
+ const { data: sessionData, error: sessionError } = await supabase.auth.getSession()
+ results.tests.currentSession = {
+ success: !sessionError,
+ hasSession: !!sessionData.session,
+ error: sessionError?.message,
+ userId: sessionData.session?.user?.id,
+ email: sessionData.session?.user?.email
+ }
+
+ // Test 2: Get current user
+ console.log('[Auth Debug] Testing current user...')
+ const { data: userData, error: userError } = await supabase.auth.getUser()
+ results.tests.currentUser = {
+ success: !userError,
+ hasUser: !!userData.user,
+ error: userError?.message,
+ userId: userData.user?.id,
+ email: userData.user?.email
+ }
+
+ // Test 3: Environment variables
+ results.tests.environment = {
+ supabaseUrl: import.meta.env.VITE_SUPABASE_URL || 'MISSING',
+ supabaseKey: import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY ? 'CONFIGURED' : 'MISSING',
+ mode: import.meta.env.MODE,
+ dev: import.meta.env.DEV
+ }
+
+ // Test 4: Local storage
+ results.tests.localStorage = {
+ hasSupabaseSession: !!localStorage.getItem('sb-nrlkjfznsavsbmweiyqu-auth-token'),
+ keys: Object.keys(localStorage).filter(key => key.includes('supabase') || key.includes('sb-'))
+ }
+
+ // Test 5: Context state
+ results.tests.contextState = {
+ hasSession: !!session,
+ hasUser: !!user,
+ loading,
+ hasError: !!error,
+ errorMessage: error?.message
+ }
+
+ } catch (err) {
+ results.tests.unexpectedError = {
+ message: err instanceof Error ? err.message : 'Unknown error',
+ error: err
+ }
+ }
+
+ console.log('[Auth Debug] Test results:', results)
+ setTestResults(results)
+ setIsTestingAuth(false)
+ }
+
+ const clearLocalStorage = () => {
+ const keys = Object.keys(localStorage).filter(key =>
+ key.includes('supabase') || key.includes('sb-')
+ )
+
+ keys.forEach(key => localStorage.removeItem(key))
+
+ console.log('[Auth Debug] Cleared local storage keys:', keys)
+ alert(`Cleared ${keys.length} auth-related localStorage keys`)
+ }
+
+ const testSignIn = async () => {
+ try {
+ const redirectTo = `${window.location.origin}/auth/callback`
+ const { error } = await supabase.auth.signInWithOAuth({
+ provider: 'github',
+ options: { redirectTo }
+ })
+
+ if (error) {
+ alert(`Sign in test failed: ${error.message}`)
+ }
+ } catch (err) {
+ alert(`Sign in test error: ${err instanceof Error ? err.message : 'Unknown error'}`)
+ }
+ }
+
+ const testApiRequest = async () => {
+ if (!session?.access_token) {
+ setApiResponse({
+ error: 'No JWT token available. Please sign in first.',
+ timestamp: new Date().toISOString()
+ })
+ return
+ }
+
+ setIsTestingApi(true)
+ setApiResponse(null)
+
+ const requestData = {
+ url: apiUrl,
+ method: apiMethod,
+ timestamp: new Date().toISOString(),
+ jwt: session.access_token.substring(0, 20) + '...' // Show partial token for debug
+ }
+
+ try {
+ console.log('[API Debug] Making request with JWT:', requestData)
+
+ const requestOptions: RequestInit = {
+ method: apiMethod,
+ headers: {
+ 'Authorization': `Bearer ${session.access_token}`,
+ 'Content-Type': 'application/json',
+ }
+ }
+
+ // Add request body for POST/PUT requests
+ if ((apiMethod === 'POST' || apiMethod === 'PUT') && apiRequestBody.trim()) {
+ try {
+ JSON.parse(apiRequestBody) // Validate JSON
+ requestOptions.body = apiRequestBody
+ } catch (e) {
+ setApiResponse({
+ error: 'Invalid JSON in request body',
+ timestamp: new Date().toISOString(),
+ requestData
+ })
+ return
+ }
+ }
+
+ const response = await fetch(apiUrl, requestOptions)
+
+ let responseData: any
+ const contentType = response.headers.get('content-type')
+
+ if (contentType && contentType.includes('application/json')) {
+ responseData = await response.json()
+ } else {
+ responseData = await response.text()
+ }
+
+ const result = {
+ success: response.ok,
+ status: response.status,
+ statusText: response.statusText,
+ headers: Object.fromEntries(response.headers.entries()),
+ data: responseData,
+ requestData,
+ timestamp: new Date().toISOString()
+ }
+
+ console.log('[API Debug] Response:', result)
+ setApiResponse(result)
+
+ } catch (err) {
+ const errorResult = {
+ error: err instanceof Error ? err.message : 'Unknown error',
+ requestData,
+ timestamp: new Date().toISOString()
+ }
+
+ console.error('[API Debug] Request failed:', errorResult)
+ setApiResponse(errorResult)
+ } finally {
+ setIsTestingApi(false)
+ }
+ }
+
+ return (
+
+
+
+ {/* Header */}
+
+
+ Authentication Debug Panel
+
+
+ Debug and test authentication functionality
+
+
+
+ {/* Current Auth State */}
+
+
+ Current Authentication State
+
+
+
+
+
Loading
+
+ {loading ? 'Yes' : 'No'}
+
+
+
+
+
Has Session
+
+ {session ? 'Yes' : 'No'}
+
+
+
+
+
User ID
+
+ {user?.id || 'None'}
+
+
+
+
+
Email
+
+ {user?.email || 'None'}
+
+
+
+
+ {error && (
+
+
Authentication Error
+
{error.message}
+
+ )}
+
+ {session && (
+
+
+ Full Session Data
+
+
+ {JSON.stringify(session, null, 2)}
+
+
+ )}
+
+
+ {/* Actions */}
+
+
Actions
+
+
+
+
+
+
+
+
+ {session && (
+
+ )}
+
+
+
+
+
+ {/* JWT API Request Testing */}
+ {session && (
+
+
+ JWT API Request Testing
+
+
+ Test authenticated requests to your backend using the JWT token
+
+
+
+ {/* URL Input */}
+
+
+ setApiUrl(e.target.value)}
+ placeholder="https://example.com/api/v1/admin/settings"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+ {/* Method Selection */}
+
+
+
+
+
+ {/* Request Body (for POST/PUT) */}
+ {(apiMethod === 'POST' || apiMethod === 'PUT') && (
+
+
+
+ )}
+
+ {/* Token Info */}
+
+
JWT Token Info
+
+
Token: {session?.access_token ? `${session.access_token.substring(0, 30)}...` : 'No token'}
+
Expires: {session?.expires_at ? new Date(session.expires_at * 1000).toLocaleString() : 'Unknown'}
+
+
+
+ {/* Send Request Button */}
+
+
+
+ )}
+
+ {/* API Response */}
+ {apiResponse && (
+
+
API Response
+
+ {apiResponse.error ? (
+
+
Request Failed
+
{apiResponse.error}
+
+ ) : (
+
+
+ {apiResponse.status} {apiResponse.statusText}
+
+
+ Request {apiResponse.success ? 'successful' : 'completed with non-2xx status'}
+
+
+ )}
+
+
+ {/* Response Headers */}
+ {apiResponse.headers && (
+
+
+ Response Headers
+
+
+ {JSON.stringify(apiResponse.headers, null, 2)}
+
+
+ )}
+
+ {/* Response Data */}
+
+
+ Response Data
+
+
+ {typeof apiResponse.data === 'string'
+ ? apiResponse.data
+ : JSON.stringify(apiResponse.data, null, 2)}
+
+
+
+ {/* Request Debug Info */}
+ {apiResponse.requestData && (
+
+
+ Request Debug Info
+
+
+ {JSON.stringify(apiResponse.requestData, null, 2)}
+
+
+ )}
+
+
+ )}
+
+ {/* Test Results */}
+ {testResults && (
+
+
Test Results
+
+ {JSON.stringify(testResults, null, 2)}
+
+
+ )}
+
+ {/* Environment Info */}
+
+
Environment
+
+
Mode: {import.meta.env.MODE}
+
Dev: {import.meta.env.DEV ? 'Yes' : 'No'}
+
Supabase URL: {import.meta.env.VITE_SUPABASE_URL || 'NOT SET'}
+
Supabase Key: {import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY ? 'CONFIGURED' : 'NOT SET'}
+
Origin: {window.location.origin}
+
Callback URL: {window.location.origin}/auth/callback
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/routes/Login.tsx b/frontend/src/routes/Login.tsx
new file mode 100644
index 000000000..3ab338c45
--- /dev/null
+++ b/frontend/src/routes/Login.tsx
@@ -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(null)
+ const [debugInfo, setDebugInfo] = useState(null)
+
+ // Show logged in state instead of redirecting
+ if (session && !loading) {
+ return (
+
+
+
+
✅
+
+ YOU ARE LOGGED IN
+
+
+ Successfully authenticated with Supabase
+
+
+
+
+ {/* User Info Cards */}
+
+
+
User ID
+
+ {user?.id}
+
+
+
+
+
Email
+
+ {user?.email}
+
+
+
+
+
Provider
+
+ {user?.app_metadata?.provider || 'Unknown'}
+
+
+
+
+
Created
+
+ {user?.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'}
+
+
+
+
+ {/* JWT Token Display */}
+
+
JWT Access Token
+
+ {session?.access_token}
+
+
+ Expires: {session?.expires_at ? new Date(session.expires_at * 1000).toLocaleString() : 'Unknown'}
+
+
+
+ {/* Refresh Token (if available) */}
+ {session?.refresh_token && (
+
+
Refresh Token
+
+ {session.refresh_token}
+
+
+ )}
+
+ {/* User Metadata */}
+ {(user?.user_metadata && Object.keys(user.user_metadata).length > 0) && (
+
+
+ User Metadata
+
+
+ {JSON.stringify(user.user_metadata, null, 2)}
+
+
+ )}
+
+ {/* App Metadata */}
+ {(user?.app_metadata && Object.keys(user.app_metadata).length > 0) && (
+
+
+ App Metadata
+
+
+ {JSON.stringify(user.app_metadata, null, 2)}
+
+
+ )}
+
+ {/* Full Session Data */}
+
+
+ Full Session Object
+
+
+ {JSON.stringify(session, null, 2)}
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ 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 (
+
+ )
+ }
+
+ return (
+
+
+
+
+ Welcome to Stirling PDF
+
+
+ Sign in to access your account
+
+
+
+ {error && (
+
+ )}
+
+
+
+
+ {import.meta.env.DEV && (
+
+
Development Tools
+
+
+
+
+
Environment: {import.meta.env.MODE}
+
Supabase URL: {import.meta.env.VITE_SUPABASE_URL ? '✓ Configured' : '✗ Missing'}
+
Supabase Key: {import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY ? '✓ Configured' : '✗ Missing'}
+
+
+ )}
+
+
+ {debugInfo && import.meta.env.DEV && (
+
+
+ Debug Information
+
+
+ {JSON.stringify(debugInfo, null, 2)}
+
+
+ )}
+
+
+
+ This is a demo login page for testing authentication
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/routes/LoginTest.tsx b/frontend/src/routes/LoginTest.tsx
new file mode 100644
index 000000000..6f172b836
--- /dev/null
+++ b/frontend/src/routes/LoginTest.tsx
@@ -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(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 (
+
+
+
+ Login Test Page
+
+
+
+
+
Auth Status
+
+
Loading: {loading ? 'Yes' : 'No'}
+
Has Session: {session ? 'Yes' : 'No'}
+
Error: {error ? error.message : 'None'}
+
+
+
+
+
+ {debugInfo && (
+
+
+ Debug Info
+
+
+ {JSON.stringify(debugInfo, null, 2)}
+
+
+ )}
+
+
+
+ If you can see this page, routing is working
+
+
+ Path: {window.location.pathname}
+
+
+
+
+
+ )
+}
\ No newline at end of file