You've already forked agentic-coding-workflow
feat(web): fetch-based api client with 401 handling and tests
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { request } from './client'
|
||||
import type { User } from '@/stores/auth'
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: User
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
login: (email: string, password: string) =>
|
||||
request<LoginResponse>('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
body: { email, password },
|
||||
}),
|
||||
me: () => request<User>('/api/v1/auth/me'),
|
||||
logout: () => request<void>('/api/v1/auth/logout', { method: 'POST' }),
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ApiException, type ApiError } from './types'
|
||||
|
||||
const BASE = '' // 同源;dev 走 vite proxy
|
||||
|
||||
interface RequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'
|
||||
body?: unknown
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
let authTokenProvider: () => string | null = () => null
|
||||
let onUnauthorized: () => void = () => {}
|
||||
|
||||
export function configure(opts: {
|
||||
tokenProvider: () => string | null
|
||||
onUnauthorized: () => void
|
||||
}) {
|
||||
authTokenProvider = opts.tokenProvider
|
||||
onUnauthorized = opts.onUnauthorized
|
||||
}
|
||||
|
||||
export async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-Request-ID': crypto.randomUUID(),
|
||||
}
|
||||
const token = authTokenProvider()
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
|
||||
const resp = await fetch(BASE + path, {
|
||||
method: opts.method || 'GET',
|
||||
headers,
|
||||
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
||||
signal: opts.signal,
|
||||
})
|
||||
|
||||
if (resp.status === 401) {
|
||||
onUnauthorized()
|
||||
}
|
||||
|
||||
if (resp.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
const text = await resp.text()
|
||||
const data = text ? JSON.parse(text) : null
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new ApiException(
|
||||
resp.status,
|
||||
(data as ApiError) || { code: 'unknown', message: resp.statusText },
|
||||
)
|
||||
}
|
||||
return data as T
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { request } from './client'
|
||||
import type { Notification } from '@/stores/notifications'
|
||||
|
||||
export const notificationsApi = {
|
||||
list: (unreadOnly = false, limit = 50) =>
|
||||
request<{ items: Notification[] }>(
|
||||
`/api/v1/notifications?unread=${unreadOnly}&limit=${limit}`,
|
||||
),
|
||||
unreadCount: () =>
|
||||
request<{ count: number }>('/api/v1/notifications/unread-count'),
|
||||
markRead: (id: string) =>
|
||||
request<void>(`/api/v1/notifications/${id}/read`, { method: 'PATCH' }),
|
||||
markAllRead: () =>
|
||||
request<void>('/api/v1/notifications/read-all', { method: 'PATCH' }),
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface ApiError {
|
||||
code: string
|
||||
message: string
|
||||
fields?: Record<string, unknown>
|
||||
request_id?: string
|
||||
}
|
||||
|
||||
export class ApiException extends Error {
|
||||
status: number
|
||||
body: ApiError
|
||||
|
||||
constructor(status: number, body: ApiError) {
|
||||
super(body.message || `HTTP ${status}`)
|
||||
this.status = status
|
||||
this.body = body
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user