diff --git a/web/lib/api.ts b/web/lib/api.ts new file mode 100644 index 0000000..6bc1b16 --- /dev/null +++ b/web/lib/api.ts @@ -0,0 +1,141 @@ +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080'; + +class ApiClient { + private accessToken: string | null = null; + + setAccessToken(token: string | null) { + this.accessToken = token; + } + + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...((options.headers as Record) || {}), + }; + + if (this.accessToken) { + headers['Authorization'] = `Bearer ${this.accessToken}`; + } + + const response = await fetch(`${API_URL}${endpoint}`, { + ...options, + headers, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Request failed' })); + throw new Error(error.message || `HTTP error ${response.status}`); + } + + if (response.status === 204) { + return undefined as T; + } + + return response.json(); + } + + // Auth endpoints + async register(email: string, password: string, displayName: string) { + return this.request('/v1/auth/register', { + method: 'POST', + body: JSON.stringify({ email, password, displayName }), + }); + } + + async login(email: string, password: string, orgId?: string) { + return this.request('/v1/auth/login', { + method: 'POST', + body: JSON.stringify({ email, password, orgId }), + }); + } + + async refresh(refreshToken: string) { + return this.request('/v1/auth/refresh', { + method: 'POST', + body: JSON.stringify({ refreshToken }), + }); + } + + async switchOrg(refreshToken: string, orgId: string) { + return this.request('/v1/auth/switch-org', { + method: 'POST', + body: JSON.stringify({ refreshToken, orgId }), + }); + } + + async logout(refreshToken: string) { + return this.request('/v1/auth/logout', { + method: 'POST', + body: JSON.stringify({ refreshToken }), + }); + } + + async getMe() { + return this.request('/v1/me'); + } + + // Incidents endpoints + async getIncidents(status?: string, cursor?: string, limit = 20) { + const params = new URLSearchParams(); + if (status) params.set('status', status); + if (cursor) params.set('cursor', cursor); + params.set('limit', limit.toString()); + return this.request<{ items: import('@/types').Incident[]; nextCursor?: string }>( + `/v1/incidents?${params}` + ); + } + + async getIncident(id: string) { + return this.request(`/v1/incidents/${id}`); + } + + async getIncidentEvents(id: string) { + return this.request(`/v1/incidents/${id}/events`); + } + + async createIncident(serviceId: string, title: string, description?: string) { + return this.request(`/v1/services/${serviceId}/incidents`, { + method: 'POST', + body: JSON.stringify({ title, description }), + }); + } + + async transitionIncident(id: string, action: string, expectedVersion: number) { + return this.request(`/v1/incidents/${id}/transition`, { + method: 'POST', + body: JSON.stringify({ action, expectedVersion }), + }); + } + + async addComment(id: string, content: string) { + return this.request(`/v1/incidents/${id}/comment`, { + method: 'POST', + body: JSON.stringify({ content }), + }); + } + + // Org endpoints + async getOrg() { + return this.request('/v1/org'); + } + + async getOrgMembers() { + return this.request('/v1/org/members'); + } + + async getServices() { + return this.request('/v1/org/services'); + } + + async createService(name: string, slug: string, description?: string) { + return this.request('/v1/org/services', { + method: 'POST', + body: JSON.stringify({ name, slug, description }), + }); + } +} + +export const api = new ApiClient();