feat: implement authentication flow with login, registration, and OAuth support
This commit is contained in:
209
src/lib/api.ts
Normal file
209
src/lib/api.ts
Normal 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 }
|
||||
Reference in New Issue
Block a user