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:
155
src/lib/api/services/auth.ts
Normal file
155
src/lib/api/services/auth.ts
Normal 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()
|
||||
94
src/lib/api/services/playlists.ts
Normal file
94
src/lib/api/services/playlists.ts
Normal 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()
|
||||
100
src/lib/api/services/sounds.ts
Normal file
100
src/lib/api/services/sounds.ts
Normal 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()
|
||||
74
src/lib/api/services/users.ts
Normal file
74
src/lib/api/services/users.ts
Normal 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()
|
||||
Reference in New Issue
Block a user