Refactor API structure and integrate new modular API client

- Replaced legacy apiService with a new modular api client structure.
- Updated AuthContext, OAuthButtons, and AuthCallbackPage to use the new api client.
- Created separate services for auth, sounds, playlists, and users.
- Implemented centralized API configuration and error handling.
- Added support for OAuth providers and token exchange.
- Introduced a Toaster component for notifications in App.
- Updated API endpoints and request handling for better maintainability.
This commit is contained in:
JSC
2025-07-26 19:21:36 +02:00
parent 57429f9414
commit 6ce83c8317
15 changed files with 1055 additions and 236 deletions

View File

@@ -0,0 +1,155 @@
import type { User } from '@/types/auth'
import { apiClient } from '../client'
import { API_CONFIG } from '../config'
export interface LoginRequest {
email: string
password: string
}
export interface RegisterRequest {
email: string
password: string
name: string
}
export interface OAuthProvider {
name: string
display_name?: string
}
export interface OAuthAuthorizationResponse {
authorization_url: string
state: string
}
export interface OAuthProvidersResponse {
providers: string[]
}
export interface ExchangeOAuthTokenRequest {
code: string
}
export interface ExchangeOAuthTokenResponse {
message: string
user_id: string
}
export class AuthService {
/**
* Authenticate user with email and password
*/
async login(credentials: LoginRequest): Promise<User> {
// Using direct fetch for auth endpoints to avoid circular dependency with token refresh
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.LOGIN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
credentials: 'include',
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.detail || 'Login failed')
}
return await response.json()
}
/**
* Register a new user account
*/
async register(userData: RegisterRequest): Promise<User> {
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.REGISTER}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
credentials: 'include',
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.detail || 'Registration failed')
}
return await response.json()
}
/**
* Get current authenticated user
*/
async getMe(): Promise<User> {
return apiClient.get<User>(API_CONFIG.ENDPOINTS.AUTH.ME)
}
/**
* Logout the current user
*/
async logout(): Promise<void> {
try {
await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.LOGOUT}`, {
method: 'POST',
credentials: 'include',
})
} catch (error) {
console.error('Logout request failed:', error)
}
}
/**
* Get OAuth authorization URL for a provider
*/
async getOAuthUrl(provider: string): Promise<OAuthAuthorizationResponse> {
return apiClient.get<OAuthAuthorizationResponse>(
API_CONFIG.ENDPOINTS.AUTH.OAUTH_AUTHORIZE(provider),
{ skipAuth: true }
)
}
/**
* Get list of available OAuth providers
*/
async getOAuthProviders(): Promise<OAuthProvidersResponse> {
return apiClient.get<OAuthProvidersResponse>(
API_CONFIG.ENDPOINTS.AUTH.PROVIDERS,
{ skipAuth: true }
)
}
/**
* Exchange OAuth temporary code for auth cookies
*/
async exchangeOAuthToken(request: ExchangeOAuthTokenRequest): Promise<ExchangeOAuthTokenResponse> {
// Use direct fetch for OAuth token exchange to avoid auth loops and ensure cookies are set
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.EXCHANGE_OAUTH_TOKEN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
credentials: 'include', // Essential for receiving auth cookies
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.detail || 'OAuth token exchange failed')
}
return await response.json()
}
/**
* Refresh authentication token
*/
async refreshToken(): Promise<void> {
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.AUTH.REFRESH}`, {
method: 'POST',
credentials: 'include',
})
if (!response.ok) {
throw new Error('Token refresh failed')
}
}
}
export const authService = new AuthService()

View File

@@ -0,0 +1,94 @@
import { apiClient } from '../client'
import { API_CONFIG } from '../config'
import type { PaginatedResponse } from '../types'
import type { Sound } from './sounds'
export interface Playlist {
id: number
name: string
description?: string
user_id: number
is_public: boolean
sounds: Sound[]
created_at: string
updated_at: string
}
export interface CreatePlaylistRequest {
name: string
description?: string
is_public?: boolean
}
export interface UpdatePlaylistRequest {
name?: string
description?: string
is_public?: boolean
}
export interface PlaylistsListParams {
page?: number
size?: number
search?: string
user_id?: number
is_public?: boolean
}
export interface AddSoundToPlaylistRequest {
sound_id: number
}
export class PlaylistsService {
/**
* Get list of playlists with pagination
*/
async list(params?: PlaylistsListParams): Promise<PaginatedResponse<Playlist>> {
return apiClient.get<PaginatedResponse<Playlist>>(API_CONFIG.ENDPOINTS.PLAYLISTS.LIST, {
params: params as Record<string, string | number | boolean | undefined>
})
}
/**
* Get a specific playlist by ID
*/
async get(id: string | number): Promise<Playlist> {
return apiClient.get<Playlist>(API_CONFIG.ENDPOINTS.PLAYLISTS.GET(id))
}
/**
* Create a new playlist
*/
async create(data: CreatePlaylistRequest): Promise<Playlist> {
return apiClient.post<Playlist>(API_CONFIG.ENDPOINTS.PLAYLISTS.CREATE, data)
}
/**
* Update an existing playlist
*/
async update(id: string | number, data: UpdatePlaylistRequest): Promise<Playlist> {
return apiClient.patch<Playlist>(API_CONFIG.ENDPOINTS.PLAYLISTS.UPDATE(id), data)
}
/**
* Delete a playlist
*/
async delete(id: string | number): Promise<void> {
return apiClient.delete<void>(API_CONFIG.ENDPOINTS.PLAYLISTS.DELETE(id))
}
/**
* Add a sound to a playlist
*/
async addSound(playlistId: string | number, data: AddSoundToPlaylistRequest): Promise<Playlist> {
return apiClient.post<Playlist>(`${API_CONFIG.ENDPOINTS.PLAYLISTS.GET(playlistId)}/sounds`, data)
}
/**
* Remove a sound from a playlist
*/
async removeSound(playlistId: string | number, soundId: string | number): Promise<Playlist> {
return apiClient.delete<Playlist>(`${API_CONFIG.ENDPOINTS.PLAYLISTS.GET(playlistId)}/sounds/${soundId}`)
}
}
export const playlistsService = new PlaylistsService()

View File

@@ -0,0 +1,100 @@
import { apiClient } from '../client'
import { API_CONFIG } from '../config'
import type { PaginatedResponse } from '../types'
export interface Sound {
id: number
title: string
description?: string
file_url: string
duration: number
file_size: number
mime_type: string
play_count: number
user_id: number
created_at: string
updated_at: string
}
export interface CreateSoundRequest {
title: string
description?: string
file: File
}
export interface UpdateSoundRequest {
title?: string
description?: string
}
export interface SoundsListParams {
page?: number
size?: number
search?: string
user_id?: number
}
export class SoundsService {
/**
* Get list of sounds with pagination
*/
async list(params?: SoundsListParams): Promise<PaginatedResponse<Sound>> {
return apiClient.get<PaginatedResponse<Sound>>(API_CONFIG.ENDPOINTS.SOUNDS.LIST, {
params: params as Record<string, string | number | boolean | undefined>
})
}
/**
* Get a specific sound by ID
*/
async get(id: string | number): Promise<Sound> {
return apiClient.get<Sound>(API_CONFIG.ENDPOINTS.SOUNDS.GET(id))
}
/**
* Create a new sound
*/
async create(data: CreateSoundRequest): Promise<Sound> {
const formData = new FormData()
formData.append('title', data.title)
if (data.description) {
formData.append('description', data.description)
}
formData.append('file', data.file)
return apiClient.post<Sound>(API_CONFIG.ENDPOINTS.SOUNDS.CREATE, formData)
}
/**
* Update an existing sound
*/
async update(id: string | number, data: UpdateSoundRequest): Promise<Sound> {
return apiClient.patch<Sound>(API_CONFIG.ENDPOINTS.SOUNDS.UPDATE(id), data)
}
/**
* Delete a sound
*/
async delete(id: string | number): Promise<void> {
return apiClient.delete<void>(API_CONFIG.ENDPOINTS.SOUNDS.DELETE(id))
}
/**
* Upload a sound file
*/
async upload(file: File, metadata?: { title?: string; description?: string }): Promise<Sound> {
const formData = new FormData()
formData.append('file', file)
if (metadata?.title) {
formData.append('title', metadata.title)
}
if (metadata?.description) {
formData.append('description', metadata.description)
}
return apiClient.post<Sound>(API_CONFIG.ENDPOINTS.SOUNDS.UPLOAD, formData)
}
}
export const soundsService = new SoundsService()

View File

@@ -0,0 +1,74 @@
import type { User } from '@/types/auth'
import { apiClient } from '../client'
import { API_CONFIG } from '../config'
import type { PaginatedResponse } from '../types'
export interface UpdateUserRequest {
name?: string
email?: string
picture?: string
}
export interface UsersListParams {
page?: number
size?: number
search?: string
role?: string
is_active?: boolean
}
export interface ChangePasswordRequest {
current_password: string
new_password: string
}
export class UsersService {
/**
* Get list of users with pagination (admin only)
*/
async list(params?: UsersListParams): Promise<PaginatedResponse<User>> {
return apiClient.get<PaginatedResponse<User>>(API_CONFIG.ENDPOINTS.USERS.LIST, {
params: params as Record<string, string | number | boolean | undefined>
})
}
/**
* Get a specific user by ID
*/
async get(id: string | number): Promise<User> {
return apiClient.get<User>(API_CONFIG.ENDPOINTS.USERS.GET(id))
}
/**
* Update user profile
*/
async update(id: string | number, data: UpdateUserRequest): Promise<User> {
return apiClient.patch<User>(API_CONFIG.ENDPOINTS.USERS.UPDATE(id), data)
}
/**
* Delete a user (admin only)
*/
async delete(id: string | number): Promise<void> {
return apiClient.delete<void>(API_CONFIG.ENDPOINTS.USERS.DELETE(id))
}
/**
* Change user password
*/
async changePassword(userId: string | number, data: ChangePasswordRequest): Promise<void> {
return apiClient.post<void>(`${API_CONFIG.ENDPOINTS.USERS.GET(userId)}/change-password`, data)
}
/**
* Upload user avatar
*/
async uploadAvatar(userId: string | number, file: File): Promise<User> {
const formData = new FormData()
formData.append('avatar', file)
return apiClient.post<User>(`${API_CONFIG.ENDPOINTS.USERS.GET(userId)}/avatar`, formData)
}
}
export const usersService = new UsersService()