feat: implement authentication flow with login, registration, and OAuth support

This commit is contained in:
JSC
2025-07-26 18:37:47 +02:00
parent 12cb39503b
commit 57429f9414
11 changed files with 924 additions and 1 deletions

View File

@@ -0,0 +1,102 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router'
import { useAuth } from '@/contexts/AuthContext'
import { apiService } from '@/lib/api'
export function AuthCallbackPage() {
const navigate = useNavigate()
const { setUser } = useAuth()
const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing')
const [error, setError] = useState<string>('')
useEffect(() => {
const handleOAuthCallback = async () => {
try {
// Get the code from URL parameters
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
if (!code) {
throw new Error('No authorization code received')
}
console.log('Exchanging OAuth code for tokens...')
// Exchange the temporary code for proper auth cookies
const response = await fetch('http://localhost:8000/api/v1/auth/exchange-oauth-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
credentials: 'include', // Important for setting cookies
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.detail || 'Token exchange failed')
}
const result = await response.json()
console.log('Token exchange successful:', result)
// Now get the user info
const user = await apiService.getMe()
console.log('User info retrieved:', user)
// Update auth context
if (setUser) setUser(user)
setStatus('success')
// Redirect to dashboard after a short delay
setTimeout(() => {
navigate('/')
}, 1000)
} catch (error) {
console.error('OAuth callback failed:', error)
setError(error instanceof Error ? error.message : 'Authentication failed')
setStatus('error')
// Redirect to login after error
setTimeout(() => {
navigate('/login')
}, 3000)
}
}
handleOAuthCallback()
}, [navigate, setUser])
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-md w-full space-y-6 text-center">
{status === 'processing' && (
<div>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<h2 className="mt-4 text-xl font-semibold">Completing sign in...</h2>
<p className="text-gray-600 dark:text-gray-400">Please wait while we set up your account.</p>
</div>
)}
{status === 'success' && (
<div>
<div className="text-green-600 text-4xl mb-4"></div>
<h2 className="text-xl font-semibold text-green-600">Sign in successful!</h2>
<p className="text-gray-600 dark:text-gray-400">Redirecting to dashboard...</p>
</div>
)}
{status === 'error' && (
<div>
<div className="text-red-600 text-4xl mb-4"></div>
<h2 className="text-xl font-semibold text-red-600">Sign in failed</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">{error}</p>
<p className="text-sm text-gray-500">Redirecting to login page...</p>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
import { useAuth } from '../contexts/AuthContext'
import { ModeToggle } from '../components/ModeToggle'
export function DashboardPage() {
const { user, logout } = useAuth()
return (
<div className="min-h-screen">
<nav className="shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center">
<h1 className="text-xl font-semibold">Soundboard</h1>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-700 dark:text-gray-300">
Welcome, {user?.name}
</span>
<ModeToggle />
<button
onClick={() => logout()}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
Sign out
</button>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="border-4 border-dashed border-gray-200 dark:border-gray-700 rounded-lg h-96 flex items-center justify-center">
<p className="text-gray-500 dark:text-gray-400">Dashboard content coming soon...</p>
</div>
</div>
</main>
</div>
)
}

24
src/pages/LoginPage.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { Link } from 'react-router'
import { LoginForm } from '@/components/auth/LoginForm'
export function LoginPage() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="w-full max-w-md space-y-6">
<LoginForm />
<div className="text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">
Don't have an account?{' '}
<Link
to="/register"
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
Sign up
</Link>
</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { Link } from 'react-router'
import { RegisterForm } from '@/components/auth/RegisterForm'
export function RegisterPage() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="w-full max-w-md space-y-6">
<RegisterForm />
<div className="text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">
Already have an account?{' '}
<Link
to="/login"
className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
Sign in
</Link>
</p>
</div>
</div>
</div>
)
}