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

209
src/lib/api.ts Normal file
View File

@@ -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<void> | null = null
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${API_BASE_URL}${endpoint}`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string>),
}
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<void> {
if (this.refreshPromise) {
await this.refreshPromise
return
}
this.refreshPromise = this.performTokenRefresh()
try {
await this.refreshPromise
} finally {
this.refreshPromise = null
}
}
private async performTokenRefresh(): Promise<void> {
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<User> {
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 }