feat: initial sketch with AI
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { loginApi, refreshApi, logoutApi, logoutAllApi, API_BASE_URL } from '../auth'
|
||||
|
||||
describe('auth API', () => {
|
||||
const originalFetch = global.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
|
||||
describe('loginApi', () => {
|
||||
it('sends POST request to /login and returns token pair on success', async () => {
|
||||
const mockResponse = {
|
||||
access_token: 'access-123',
|
||||
refresh_token: 'refresh-456',
|
||||
}
|
||||
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse,
|
||||
} as unknown as Response)
|
||||
|
||||
const payload = { email: 'user@example.com', password: 'secretpassword' }
|
||||
const result = await loginApi(payload)
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(`${API_BASE_URL}/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
expect(result).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
it('throws error with message on failure', async () => {
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: async () => ({ message: 'Invalid email or password' }),
|
||||
} as unknown as Response)
|
||||
|
||||
await expect(
|
||||
loginApi({ email: 'user@example.com', password: 'wrong' }),
|
||||
).rejects.toThrow('Invalid email or password')
|
||||
})
|
||||
})
|
||||
|
||||
describe('refreshApi', () => {
|
||||
it('sends POST request to /login/refresh and returns token pair on success', async () => {
|
||||
const mockResponse = {
|
||||
access_token: 'new-access-123',
|
||||
refresh_token: 'new-refresh-456',
|
||||
}
|
||||
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse,
|
||||
} as unknown as Response)
|
||||
|
||||
const payload = { refresh_token: 'refresh-456' }
|
||||
const result = await refreshApi(payload)
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(`${API_BASE_URL}/login/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
expect(result).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
it('throws error on failure', async () => {
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
json: async () => {
|
||||
throw new Error('Not JSON')
|
||||
},
|
||||
} as unknown as Response)
|
||||
|
||||
await expect(
|
||||
refreshApi({ refresh_token: 'invalid-token' }),
|
||||
).rejects.toThrow('Unauthorized')
|
||||
})
|
||||
})
|
||||
|
||||
describe('logoutApi', () => {
|
||||
it('sends POST request to /logout on success', async () => {
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => null,
|
||||
} as unknown as Response)
|
||||
|
||||
const payload = { refresh_token: 'refresh-456' }
|
||||
await logoutApi(payload)
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(`${API_BASE_URL}/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error on failure', async () => {
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
json: async () => ({ error: 'failed to logout' }),
|
||||
} as unknown as Response)
|
||||
|
||||
await expect(
|
||||
logoutApi({ refresh_token: 'invalid-token' }),
|
||||
).rejects.toThrow('failed to logout')
|
||||
})
|
||||
})
|
||||
|
||||
describe('logoutAllApi', () => {
|
||||
it('sends POST request to /logout/all with token header on success', async () => {
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => null,
|
||||
} as unknown as Response)
|
||||
|
||||
await logoutAllApi('access-123')
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(`${API_BASE_URL}/logout/all`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer access-123',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error on failure', async () => {
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
json: async () => ({ message: 'Unauthorized' }),
|
||||
} as unknown as Response)
|
||||
|
||||
await expect(logoutAllApi('bad-token')).rejects.toThrow('Unauthorized')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { fetchWithAuth, apiClient } from '../client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import * as authApi from '@/api/auth'
|
||||
|
||||
describe('api client (fetchWithAuth)', () => {
|
||||
const originalFetch = global.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('adds Authorization header if accessToken is available in store', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setTokens({
|
||||
access_token: 'test-token',
|
||||
refresh_token: 'refresh-token',
|
||||
})
|
||||
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ success: true }),
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await fetchWithAuth('/lists')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalled()
|
||||
const callArgs = fetchMock.mock.calls[0]
|
||||
expect(callArgs).toBeDefined()
|
||||
const headers = callArgs?.[1]?.headers as Headers
|
||||
expect(headers.get('Authorization')).toBe('Bearer test-token')
|
||||
})
|
||||
|
||||
it('omits Authorization header when skipAuth is true', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setTokens({
|
||||
access_token: 'test-token',
|
||||
refresh_token: 'refresh-token',
|
||||
})
|
||||
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await fetchWithAuth('/health', { skipAuth: true })
|
||||
|
||||
const callArgs = fetchMock.mock.calls[0]
|
||||
expect(callArgs).toBeDefined()
|
||||
const headers = callArgs?.[1]?.headers as Headers
|
||||
expect(headers.has('Authorization')).toBe(false)
|
||||
})
|
||||
|
||||
it('refreshes token and retries request on 401 response', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setTokens({
|
||||
access_token: 'expired-token',
|
||||
refresh_token: 'valid-refresh-token',
|
||||
})
|
||||
|
||||
const refreshSpy = vi.spyOn(authApi, 'refreshApi').mockResolvedValueOnce({
|
||||
access_token: 'new-access-token',
|
||||
refresh_token: 'new-refresh-token',
|
||||
})
|
||||
|
||||
const firstResponse = {
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
}
|
||||
const secondResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: 'protected data' }),
|
||||
}
|
||||
|
||||
const fetchMock = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(firstResponse as unknown as Response)
|
||||
.mockResolvedValueOnce(secondResponse as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
const res = await fetchWithAuth('/lists')
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalledWith({ refresh_token: 'valid-refresh-token' })
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
expect(authStore.accessToken).toBe('new-access-token')
|
||||
|
||||
// Second call should have new access token
|
||||
const secondCall = fetchMock.mock.calls[1]
|
||||
expect(secondCall).toBeDefined()
|
||||
const secondCallHeaders = secondCall?.[1]?.headers as Headers
|
||||
expect(secondCallHeaders.get('Authorization')).toBe('Bearer new-access-token')
|
||||
expect(res).toBe(secondResponse)
|
||||
})
|
||||
|
||||
it('calls apiClient helper methods correctly', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setTokens({
|
||||
access_token: 'my-token',
|
||||
refresh_token: 'my-refresh',
|
||||
})
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
}
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(mockResponse as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await apiClient.get('/lists')
|
||||
expect(fetchMock).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining('/lists'),
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
|
||||
await apiClient.post('/lists', { name: 'My List' })
|
||||
expect(fetchMock).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining('/lists'),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'My List' }),
|
||||
}),
|
||||
)
|
||||
|
||||
await apiClient.put('/lists/item', { title: 'Updated' })
|
||||
expect(fetchMock).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining('/lists/item'),
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ title: 'Updated' }),
|
||||
}),
|
||||
)
|
||||
|
||||
await apiClient.delete('/lists/user')
|
||||
expect(fetchMock).toHaveBeenLastCalledWith(
|
||||
expect.stringContaining('/lists/user'),
|
||||
expect.objectContaining({ method: 'DELETE' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,182 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import {
|
||||
getListsApi,
|
||||
createListApi,
|
||||
getListItemsApi,
|
||||
createListItemApi,
|
||||
updateListItemApi,
|
||||
setListItemCompletedApi,
|
||||
addUserToListApi,
|
||||
removeUserFromListApi,
|
||||
} from '../lists'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { API_BASE_URL } from '@/api/auth'
|
||||
|
||||
describe('lists API', () => {
|
||||
const originalFetch = global.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
vi.restoreAllMocks()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
authStore.setTokens({ access_token: 'token-123', refresh_token: 'refresh-123' })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('getListsApi sends GET to /lists and returns the lists', async () => {
|
||||
const mockLists = [{ id: 'list-1', name: 'Groceries' }]
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockLists,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
const result = await getListsApi()
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${API_BASE_URL}/lists`,
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(result).toEqual(mockLists)
|
||||
})
|
||||
|
||||
it('getListsApi throws on failure', async () => {
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: async () => ({ message: 'Failed' }),
|
||||
} as unknown as Response)
|
||||
|
||||
await expect(getListsApi()).rejects.toThrow('Failed')
|
||||
})
|
||||
|
||||
it('getListItemsApi sends GET to /lists/{id} and returns the items', async () => {
|
||||
const mockItems = [{ id: 'item-1', list_id: 'list-1', title: 'Milk', is_completed: false }]
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockItems,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
const result = await getListItemsApi('list-1')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${API_BASE_URL}/lists/list-1`,
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(result).toEqual(mockItems)
|
||||
})
|
||||
|
||||
it('setListItemCompletedApi sends POST to /lists/items/{id}', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 204,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await setListItemCompletedApi('item-1', { is_completed: true })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${API_BASE_URL}/lists/items/item-1`,
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('createListApi sends POST to /lists and returns the created list', async () => {
|
||||
const mockList = { id: 'list-1', name: 'Groceries' }
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
json: async () => mockList,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
const result = await createListApi({ name: 'Groceries', user_ids: [] })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${API_BASE_URL}/lists`,
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
)
|
||||
expect(result).toEqual(mockList)
|
||||
})
|
||||
|
||||
it('createListApi throws on failure', async () => {
|
||||
global.fetch = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: async () => ({ message: 'Failed' }),
|
||||
} as unknown as Response)
|
||||
|
||||
await expect(createListApi({ name: 'x' })).rejects.toThrow('Failed')
|
||||
})
|
||||
|
||||
it('createListItemApi sends POST to /lists/item and returns the created item with its server id', async () => {
|
||||
const mockItem = { id: 'item-1', list_id: '', title: 'Milk', is_completed: false }
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
json: async () => mockItem,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
const result = await createListItemApi({ list_id: 'list-1', title: 'Milk' })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${API_BASE_URL}/lists/item`,
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
)
|
||||
expect(result).toEqual(mockItem)
|
||||
})
|
||||
|
||||
it('updateListItemApi sends PUT to /lists/item', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 204,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await updateListItemApi({ list_item_id: 'item-1', is_completed: true })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${API_BASE_URL}/lists/item`,
|
||||
expect.objectContaining({ method: 'PUT' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('addUserToListApi sends POST to /lists/user', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 204,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await addUserToListApi({ list_id: 'list-1', user_id: 'user-1' })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${API_BASE_URL}/lists/user`,
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('removeUserFromListApi sends DELETE to /lists/user', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 204,
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await removeUserFromListApi({ list_id: 'list-1', user_id: 'user-1' })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${API_BASE_URL}/lists/user`,
|
||||
expect.objectContaining({ method: 'DELETE' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { LoginPayload, RefreshPayload, TokenPair } from '@/types/auth'
|
||||
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api/v1'
|
||||
|
||||
async function extractErrorMessage(response: Response, fallback: string): Promise<string> {
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
return errorData.message || errorData.error || fallback
|
||||
} catch {
|
||||
return response.statusText || fallback
|
||||
}
|
||||
}
|
||||
|
||||
async function postJson(
|
||||
path: string,
|
||||
body: unknown,
|
||||
errorFallback: string,
|
||||
token?: string,
|
||||
): Promise<Response> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, errorFallback))
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export async function loginApi(payload: LoginPayload): Promise<TokenPair> {
|
||||
const response = await postJson('/login', payload, 'Login failed')
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function refreshApi(payload: RefreshPayload): Promise<TokenPair> {
|
||||
const response = await postJson('/login/refresh', payload, 'Token refresh failed')
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function logoutApi(payload?: RefreshPayload): Promise<void> {
|
||||
await postJson('/logout', payload ?? {}, 'Logout failed')
|
||||
}
|
||||
|
||||
export async function logoutAllApi(token?: string): Promise<void> {
|
||||
await postJson('/logout/all', {}, 'Logout all failed', token)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { API_BASE_URL } from '@/api/auth'
|
||||
|
||||
let refreshPromise: Promise<unknown> | null = null
|
||||
|
||||
export interface FetchOptions extends RequestInit {
|
||||
skipAuth?: boolean
|
||||
skipRefresh?: boolean
|
||||
}
|
||||
|
||||
function buildHeaders(options: RequestInit, accessToken: string | null): Headers {
|
||||
const headers = new Headers(options.headers || {})
|
||||
|
||||
if (accessToken) {
|
||||
headers.set('Authorization', `Bearer ${accessToken}`)
|
||||
}
|
||||
|
||||
if (!headers.has('Content-Type') && !(options.body instanceof FormData)) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
function buildUrl(endpoint: string): string {
|
||||
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
|
||||
return endpoint
|
||||
}
|
||||
return `${API_BASE_URL}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`
|
||||
}
|
||||
|
||||
export async function fetchWithAuth(
|
||||
endpoint: string,
|
||||
options: FetchOptions = {},
|
||||
): Promise<Response> {
|
||||
const authStore = useAuthStore()
|
||||
const { skipAuth = false, skipRefresh = false, ...customOptions } = options
|
||||
|
||||
const url = buildUrl(endpoint)
|
||||
|
||||
let response = await fetch(url, {
|
||||
...customOptions,
|
||||
headers: buildHeaders(customOptions, skipAuth ? null : authStore.accessToken),
|
||||
})
|
||||
|
||||
if (response.status === 401 && !skipRefresh && authStore.refreshToken) {
|
||||
try {
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = authStore.refreshTokens().finally(() => {
|
||||
refreshPromise = null
|
||||
})
|
||||
}
|
||||
await refreshPromise
|
||||
|
||||
response = await fetch(url, {
|
||||
...customOptions,
|
||||
headers: buildHeaders(customOptions, authStore.accessToken),
|
||||
})
|
||||
} catch {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export const apiClient = {
|
||||
get: (endpoint: string, options?: FetchOptions) =>
|
||||
fetchWithAuth(endpoint, { ...options, method: 'GET' }),
|
||||
post: (endpoint: string, body?: unknown, options?: FetchOptions) =>
|
||||
fetchWithAuth(endpoint, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
put: (endpoint: string, body?: unknown, options?: FetchOptions) =>
|
||||
fetchWithAuth(endpoint, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
delete: (endpoint: string, options?: FetchOptions) =>
|
||||
fetchWithAuth(endpoint, { ...options, method: 'DELETE' }),
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { apiClient } from '@/api/client'
|
||||
import type {
|
||||
List,
|
||||
ListItem,
|
||||
CreateListPayload,
|
||||
CreateListItemPayload,
|
||||
UpdateListItemPayload,
|
||||
SetListItemCompletedPayload,
|
||||
AddUserToListPayload,
|
||||
RemoveUserFromListPayload,
|
||||
} from '@/types/list'
|
||||
|
||||
async function extractErrorMessage(response: Response, fallback: string): Promise<string> {
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
return errorData.message || errorData.error || fallback
|
||||
} catch {
|
||||
return response.statusText || fallback
|
||||
}
|
||||
}
|
||||
|
||||
export async function getListsApi(): Promise<List[]> {
|
||||
const response = await apiClient.get('/lists')
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, 'Failed to load lists'))
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function createListApi(payload: CreateListPayload): Promise<List> {
|
||||
const response = await apiClient.post('/lists', payload)
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, 'Failed to create list'))
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function getListItemsApi(listId: string): Promise<ListItem[]> {
|
||||
const response = await apiClient.get(`/lists/${listId}`)
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, 'Failed to load list items'))
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function createListItemApi(payload: CreateListItemPayload): Promise<ListItem> {
|
||||
const response = await apiClient.post('/lists/item', payload)
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, 'Failed to create list item'))
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function updateListItemApi(payload: UpdateListItemPayload): Promise<void> {
|
||||
const response = await apiClient.put('/lists/item', payload)
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, 'Failed to update list item'))
|
||||
}
|
||||
}
|
||||
|
||||
export async function setListItemCompletedApi(
|
||||
itemId: string,
|
||||
payload: SetListItemCompletedPayload,
|
||||
): Promise<void> {
|
||||
const response = await apiClient.post(`/lists/items/${itemId}`, payload)
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, 'Failed to update list item status'))
|
||||
}
|
||||
}
|
||||
|
||||
export async function addUserToListApi(payload: AddUserToListPayload): Promise<void> {
|
||||
const response = await apiClient.post('/lists/user', payload)
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, 'Failed to add user to list'))
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeUserFromListApi(payload: RemoveUserFromListPayload): Promise<void> {
|
||||
const response = await apiClient.delete('/lists/user', {
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response, 'Failed to remove user from list'))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user