From 652a318d16e934177fe0fb33bb2bb3ed6a745d3f Mon Sep 17 00:00:00 2001 From: JSC Date: Sat, 26 Jul 2025 14:38:38 +0200 Subject: [PATCH] feat: implement authentication pages and API integration for login and registration --- src/App.tsx | 26 ++++- src/components/ui/sidebar.tsx | 2 +- src/components/ui/sonner.tsx | 2 +- src/lib/api.ts | 125 +++++++++++++++++++++ src/pages/auth/LoginPage.tsx | 193 ++++++++++++++++++++++++++++++++ src/pages/auth/RegisterPage.tsx | 160 ++++++++++++++++++++++++++ 6 files changed, 505 insertions(+), 3 deletions(-) create mode 100644 src/lib/api.ts create mode 100644 src/pages/auth/LoginPage.tsx create mode 100644 src/pages/auth/RegisterPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 9304030..9214bec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,33 @@ +import { Routes, Route } from 'react-router' +import { useSearchParams } from 'react-router' +import { useEffect } from 'react' import { ThemeProvider } from './components/ThemeProvider' +import { LoginPage } from './pages/auth/LoginPage' +import { RegisterPage } from './pages/auth/RegisterPage' + +function Dashboard() { + const [searchParams, setSearchParams] = useSearchParams() + + useEffect(() => { + if (searchParams.get('auth') === 'success') { + // Clear the auth parameter from URL + setSearchParams({}) + // You could show a success toast here + console.log('OAuth authentication successful!') + } + }, [searchParams, setSearchParams]) + + return
Dashboard - Coming Soon
+} function App() { return ( -
App
+ + } /> + } /> + } /> +
) } diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 1ee5a45..30638ac 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" -import { cva, VariantProps } from "class-variance-authority" +import { cva, type VariantProps } from "class-variance-authority" import { PanelLeftIcon } from "lucide-react" import { useIsMobile } from "@/hooks/use-mobile" diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index cd62aff..33d2d2a 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,5 +1,5 @@ import { useTheme } from "next-themes" -import { Toaster as Sonner, ToasterProps } from "sonner" +import { Toaster as Sonner, type ToasterProps } from "sonner" const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..7a7b035 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,125 @@ +const API_BASE_URL = 'http://localhost:8000/api/v1' + +export interface LoginRequest { + email: string + password: string +} + +export interface RegisterRequest { + email: string + password: string + name: string +} + +export interface UserResponse { + id: number + email: string + name: string + picture?: string + role: string + credits: number + is_active: boolean + plan: Record + created_at: string + updated_at: string +} + +class ApiError extends Error { + public status: number + public details?: unknown + + constructor( + message: string, + status: number, + details?: unknown + ) { + super(message) + this.name = 'ApiError' + this.status = status + this.details = details + } +} + +async function handleResponse(response: Response): Promise { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new ApiError( + errorData.detail || `HTTP error! status: ${response.status}`, + response.status, + errorData + ) + } + return response.json() +} + +export interface OAuthAuthorizationResponse { + authorization_url: string + state: string +} + +export interface OAuthProvidersResponse { + providers: string[] +} + +export const authApi = { + async login(data: LoginRequest): Promise { + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(data), + }) + return handleResponse(response) + }, + + async register(data: RegisterRequest): Promise { + const response = await fetch(`${API_BASE_URL}/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(data), + }) + return handleResponse(response) + }, + + async getCurrentUser(): Promise { + const response = await fetch(`${API_BASE_URL}/auth/me`, { + credentials: 'include', + }) + return handleResponse(response) + }, + + async logout(): Promise { + const response = await fetch(`${API_BASE_URL}/auth/logout`, { + method: 'POST', + credentials: 'include', + }) + await response.json() + }, + + async refreshToken(): Promise { + const response = await fetch(`${API_BASE_URL}/auth/refresh`, { + method: 'POST', + credentials: 'include', + }) + await response.json() + }, +} + +export const oauthApi = { + async getProviders(): Promise { + const response = await fetch(`${API_BASE_URL}/oauth/providers`) + return handleResponse(response) + }, + + async getAuthorizationUrl(provider: string): Promise { + const response = await fetch(`${API_BASE_URL}/oauth/${provider}/authorize`) + return handleResponse(response) + }, +} + +export { ApiError } \ No newline at end of file diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx new file mode 100644 index 0000000..ac76504 --- /dev/null +++ b/src/pages/auth/LoginPage.tsx @@ -0,0 +1,193 @@ +import { useState, useEffect } from 'react' +import { Link } from 'react-router' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Separator } from '@/components/ui/separator' +import { Github } from 'lucide-react' +import { authApi, oauthApi, type LoginRequest } from '@/lib/api' + +export function LoginPage() { + const [formData, setFormData] = useState({ + email: '', + password: '', + }) + const [isLoading, setIsLoading] = useState(false) + const [isOAuthLoading, setIsOAuthLoading] = useState(null) + const [error, setError] = useState(null) + const [providers, setProviders] = useState([]) + + useEffect(() => { + oauthApi.getProviders() + .then(response => setProviders(response.providers)) + .catch(error => console.error('Failed to load OAuth providers:', error)) + }, []) + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value, + })) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + try { + await authApi.login(formData) + // Redirect to dashboard or home page + window.location.href = '/' + } catch (error: unknown) { + setError(error instanceof Error ? error.message : 'Login failed') + } finally { + setIsLoading(false) + } + } + + const handleOAuthLogin = async (provider: string) => { + setIsOAuthLoading(provider) + setError(null) + + try { + // Get authorization URL from backend and redirect + const authResponse = await oauthApi.getAuthorizationUrl(provider) + window.location.href = authResponse.authorization_url + } catch (error: unknown) { + setError(error instanceof Error ? error.message : `${provider} login failed`) + setIsOAuthLoading(null) + } + } + + const getProviderIcon = (provider: string) => { + switch (provider.toLowerCase()) { + case 'github': + return + case 'google': + return ( + + + + + + + ) + default: + return null + } + } + + const getProviderName = (provider: string) => { + return provider.charAt(0).toUpperCase() + provider.slice(1) + } + + return ( +
+ + + + Sign in + + + Enter your email and password to access your account + + + + {error && ( +
+ {error} +
+ )} + + {/* OAuth Buttons */} + {providers.length > 0 && ( +
+ {providers.map((provider) => ( + + ))} + +
+
+ +
+
+ + Or continue with email + +
+
+
+ )} + + {/* Email/Password Form */} +
+
+ + +
+ +
+ + +
+ + +
+ +
+ Don't have an account?{' '} + + Sign up + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/pages/auth/RegisterPage.tsx b/src/pages/auth/RegisterPage.tsx new file mode 100644 index 0000000..9d5c803 --- /dev/null +++ b/src/pages/auth/RegisterPage.tsx @@ -0,0 +1,160 @@ +import { useState } from 'react' +import { Link } from 'react-router' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { authApi, type RegisterRequest } from '@/lib/api' + +export function RegisterPage() { + const [formData, setFormData] = useState({ + email: '', + password: '', + name: '', + }) + const [confirmPassword, setConfirmPassword] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + if (name === 'confirmPassword') { + setConfirmPassword(value) + } else { + setFormData(prev => ({ + ...prev, + [name]: value, + })) + } + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + if (formData.password !== confirmPassword) { + setError('Passwords do not match') + setIsLoading(false) + return + } + + if (formData.password.length < 8) { + setError('Password must be at least 8 characters long') + setIsLoading(false) + return + } + + try { + await authApi.register(formData) + // Redirect to dashboard or home page + window.location.href = '/' + } catch (error: unknown) { + setError(error instanceof Error ? error.message : 'Registration failed') + } finally { + setIsLoading(false) + } + } + + return ( +
+ + + + Create account + + + Enter your information to create a new account + + + +
+ {error && ( +
+ {error} +
+ )} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ Already have an account?{' '} + + Sign in + +
+
+
+
+ ) +} \ No newline at end of file