+ )
+}
\ No newline at end of file
diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx
new file mode 100644
index 0000000..bb966bc
--- /dev/null
+++ b/src/components/auth/RegisterForm.tsx
@@ -0,0 +1,144 @@
+import { useState } from 'react'
+import { useAuth } from '@/contexts/AuthContext'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { OAuthButtons } from './OAuthButtons'
+import { ApiError } from '@/lib/api'
+
+export function RegisterForm() {
+ const { register } = useAuth()
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ confirmPassword: '',
+ name: '',
+ })
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState('')
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+ setError('')
+
+ if (formData.password !== formData.confirmPassword) {
+ setError('Passwords do not match')
+ setLoading(false)
+ return
+ }
+
+ if (formData.password.length < 8) {
+ setError('Password must be at least 8 characters long')
+ setLoading(false)
+ return
+ }
+
+ try {
+ await register({
+ email: formData.email,
+ password: formData.password,
+ name: formData.name,
+ })
+ } catch (err) {
+ if (err instanceof ApiError) {
+ setError(err.message)
+ } else {
+ setError('An unexpected error occurred')
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData(prev => ({
+ ...prev,
+ [e.target.name]: e.target.value,
+ }))
+ }
+
+ return (
+
+
+ Create account
+
+ Enter your information to create your account
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..a3e425e
--- /dev/null
+++ b/src/contexts/AuthContext.tsx
@@ -0,0 +1,74 @@
+import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
+import { apiService } from '@/lib/api'
+import type { AuthContextType, User, LoginRequest, RegisterRequest } from '@/types/auth'
+
+const AuthContext = createContext(null)
+
+export function useAuth() {
+ const context = useContext(AuthContext)
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider')
+ }
+ return context
+}
+
+interface AuthProviderProps {
+ children: ReactNode
+}
+
+export function AuthProvider({ children }: AuthProviderProps) {
+ const [user, setUser] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ const initAuth = async () => {
+ try {
+ // Try to get user info using cookies
+ const user = await apiService.getMe()
+ setUser(user)
+ } catch {
+ // User is not authenticated - this is normal for logged out users
+ }
+ setLoading(false)
+ }
+
+ initAuth()
+ }, [])
+
+ const login = async (credentials: LoginRequest) => {
+ try {
+ const response = await apiService.login(credentials)
+ setUser(response.user)
+ } catch (error) {
+ console.error('Login failed:', error)
+ throw error
+ }
+ }
+
+ const register = async (data: RegisterRequest) => {
+ try {
+ const response = await apiService.register(data)
+ setUser(response.user)
+ } catch (error) {
+ console.error('Registration failed:', error)
+ throw error
+ }
+ }
+
+ const logout = async () => {
+ await apiService.logout()
+ setUser(null)
+ }
+
+ const value: AuthContextType = {
+ user,
+ token: user ? 'cookie-based' : null,
+ login,
+ register,
+ logout,
+ loading,
+ setUser,
+ }
+
+ return {children}
+}
\ No newline at end of file
diff --git a/src/lib/api.ts b/src/lib/api.ts
new file mode 100644
index 0000000..0520991
--- /dev/null
+++ b/src/lib/api.ts
@@ -0,0 +1,209 @@
+import type { LoginRequest, RegisterRequest } from '@/types/auth'
+
+const API_BASE_URL = 'http://localhost:8000'
+
+class ApiError extends Error {
+ public status: number
+ public response?: unknown
+
+ constructor(message: string, status: number, response?: unknown) {
+ super(message)
+ this.name = 'ApiError'
+ this.status = status
+ this.response = response
+ }
+}
+
+class ApiService {
+ private refreshPromise: Promise | null = null
+
+ private async request(
+ endpoint: string,
+ options: RequestInit = {}
+ ): Promise {
+ const url = `${API_BASE_URL}${endpoint}`
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ ...(options.headers as Record),
+ }
+
+ const config: RequestInit = {
+ ...options,
+ headers,
+ credentials: 'include', // Always include cookies
+ }
+
+ try {
+ const response = await fetch(url, config)
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ try {
+ await this.refreshToken()
+ const retryResponse = await fetch(url, config)
+
+ if (!retryResponse.ok) {
+ throw new ApiError(
+ 'Request failed after token refresh',
+ retryResponse.status,
+ await retryResponse.json().catch(() => null)
+ )
+ }
+
+ return await retryResponse.json()
+ } catch (refreshError) {
+ // Only redirect if we're not already on the login page to prevent infinite loops
+ const currentPath = window.location.pathname
+ if (currentPath !== '/login' && currentPath !== '/register') {
+ window.location.href = '/login'
+ }
+ throw refreshError
+ }
+ }
+
+ const errorData = await response.json().catch(() => null)
+ throw new ApiError(
+ errorData?.detail || 'Request failed',
+ response.status,
+ errorData
+ )
+ }
+
+ return await response.json()
+ } catch (error) {
+ if (error instanceof ApiError) {
+ throw error
+ }
+ throw new ApiError('Network error', 0)
+ }
+ }
+
+ private async refreshToken(): Promise {
+ if (this.refreshPromise) {
+ await this.refreshPromise
+ return
+ }
+
+ this.refreshPromise = this.performTokenRefresh()
+
+ try {
+ await this.refreshPromise
+ } finally {
+ this.refreshPromise = null
+ }
+ }
+
+ private async performTokenRefresh(): Promise {
+ const response = await fetch(`${API_BASE_URL}/api/v1/auth/refresh`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include', // Send cookies with refresh token
+ })
+
+ if (!response.ok) {
+ throw new ApiError('Token refresh failed', response.status)
+ }
+
+ // Token is automatically set in cookies by the backend
+ // No need to handle it manually
+ }
+
+ async login(credentials: LoginRequest): Promise<{ user: User }> {
+ const response = await fetch(`${API_BASE_URL}/api/v1/auth/login`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(credentials),
+ credentials: 'include', // Important for cookies
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => null)
+ throw new ApiError(
+ errorData?.detail || 'Login failed',
+ response.status,
+ errorData
+ )
+ }
+
+ const user = await response.json()
+
+ return { user }
+ }
+
+ async register(userData: RegisterRequest): Promise<{ user: User }> {
+ const response = await fetch(`${API_BASE_URL}/api/v1/auth/register`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(userData),
+ credentials: 'include', // Important for cookies
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => null)
+ throw new ApiError(
+ errorData?.detail || 'Registration failed',
+ response.status,
+ errorData
+ )
+ }
+
+ const user = await response.json()
+
+ return { user }
+ }
+
+ async getMe(): Promise {
+ return this.request('/api/v1/auth/me')
+ }
+
+ async logout() {
+ try {
+ await fetch(`${API_BASE_URL}/api/v1/auth/logout`, {
+ method: 'POST',
+ credentials: 'include',
+ })
+ } catch (error) {
+ console.error('Logout request failed:', error)
+ }
+ }
+
+ async getOAuthUrl(provider: string): Promise<{ authorization_url: string; state: string }> {
+ const response = await fetch(`${API_BASE_URL}/api/v1/auth/${provider}/authorize`, {
+ method: 'GET',
+ credentials: 'include',
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => null)
+ throw new ApiError(
+ errorData?.detail || 'Failed to get OAuth URL',
+ response.status,
+ errorData
+ )
+ }
+
+ return await response.json()
+ }
+
+ async getOAuthProviders(): Promise<{ providers: string[] }> {
+ const response = await fetch(`${API_BASE_URL}/api/v1/auth/providers`, {
+ method: 'GET',
+ })
+
+ if (!response.ok) {
+ throw new ApiError('Failed to get OAuth providers', response.status)
+ }
+
+ return await response.json()
+ }
+
+}
+
+export const apiService = new ApiService()
+export { ApiError }
\ No newline at end of file
diff --git a/src/pages/AuthCallbackPage.tsx b/src/pages/AuthCallbackPage.tsx
new file mode 100644
index 0000000..06bae9b
--- /dev/null
+++ b/src/pages/AuthCallbackPage.tsx
@@ -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('')
+
+ 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 (
+
+
+ {status === 'processing' && (
+
+
+
Completing sign in...
+
Please wait while we set up your account.
+
+ )}
+
+ {status === 'success' && (
+
+
✓
+
Sign in successful!
+
Redirecting to dashboard...
+
+ )}
+
+ {status === 'error' && (
+
+
✗
+
Sign in failed
+
{error}
+
Redirecting to login page...
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx
new file mode 100644
index 0000000..eff225d
--- /dev/null
+++ b/src/pages/DashboardPage.tsx
@@ -0,0 +1,38 @@
+import { useAuth } from '../contexts/AuthContext'
+import { ModeToggle } from '../components/ModeToggle'
+
+export function DashboardPage() {
+ const { user, logout } = useAuth()
+
+ return (
+
+
+
+
+
+
+
Dashboard content coming soon...
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..4b1fa35
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,24 @@
+import { Link } from 'react-router'
+import { LoginForm } from '@/components/auth/LoginForm'
+
+export function LoginPage() {
+ return (
+
+
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/pages/RegisterPage.tsx b/src/pages/RegisterPage.tsx
new file mode 100644
index 0000000..6bb4970
--- /dev/null
+++ b/src/pages/RegisterPage.tsx
@@ -0,0 +1,24 @@
+import { Link } from 'react-router'
+import { RegisterForm } from '@/components/auth/RegisterForm'
+
+export function RegisterPage() {
+ return (
+
+
+
+
+
+
+ Already have an account?{' '}
+
+ Sign in
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/types/auth.ts b/src/types/auth.ts
new file mode 100644
index 0000000..37cd99e
--- /dev/null
+++ b/src/types/auth.ts
@@ -0,0 +1,49 @@
+export interface User {
+ id: number
+ email: string
+ name: string
+ picture?: string
+ role: string
+ credits: number
+ is_active: boolean
+ plan: {
+ id: number
+ name: string
+ max_credits: number
+ features: string[]
+ }
+ created_at: string
+ updated_at: string
+}
+
+export interface TokenResponse {
+ access_token: string
+ token_type: string
+ expires_in: number
+}
+
+export interface AuthResponse {
+ user: User
+ token: TokenResponse
+}
+
+export interface LoginRequest {
+ email: string
+ password: string
+}
+
+export interface RegisterRequest {
+ email: string
+ password: string
+ name: string
+}
+
+export interface AuthContextType {
+ user: User | null
+ token: string | null
+ login: (credentials: LoginRequest) => Promise
+ register: (data: RegisterRequest) => Promise
+ logout: () => Promise
+ loading: boolean
+ setUser?: (user: User | null) => void
+}
\ No newline at end of file