const API_BASE = import.meta.env.VITE_API_URL || '/api/v1' export interface User { id: number email: string username: string full_name: string employee_id: string | null phone: string | null occupation: string | null is_active: boolean created_at: string updated_at: string } export interface AuthTokens { access_token: string refresh_token: string token_type: string user: User } export interface RegisterPayload { email: string username: string password: string full_name: string employee_id?: string phone?: string occupation?: string } export interface LoginPayload { username_or_email: string password: string remember_me?: boolean } export interface UserUpdatePayload { full_name?: string employee_id?: string phone?: string occupation?: string } class ApiError extends Error { status: number constructor(message: string, status: number) { super(message) this.status = status } } function parseErrorMessage(data: unknown, status: number): string { if (!data || typeof data !== 'object') { if (status >= 500) return 'Server error. Please ensure the backend is running.' return 'Something went wrong' } const record = data as Record const detail = record.detail if (typeof detail === 'string') return detail if (Array.isArray(detail)) { return detail .map((item) => { if (item && typeof item === 'object' && 'msg' in item) { return String((item as { msg: unknown }).msg) } return JSON.stringify(item) }) .join(', ') } if (typeof record.message === 'string') return record.message if (status >= 500) return 'Server error. Please ensure the backend is running.' return 'Something went wrong' } async function request(path: string, options: RequestInit = {}): Promise { const token = localStorage.getItem('silos_access_token') const headers: Record = { 'Content-Type': 'application/json', ...(options.headers as Record), } if (token) { headers.Authorization = `Bearer ${token}` } let response: Response try { response = await fetch(`${API_BASE}${path}`, { ...options, headers, }) } catch { throw new ApiError('Cannot reach the server. Is the backend running on port 8000?', 0) } const data = await response.json().catch(() => ({})) if (!response.ok) { throw new ApiError(parseErrorMessage(data, response.status), response.status) } if (response.status === 204) { return undefined as T } return data as T } export const authApi = { register: (payload: RegisterPayload) => request('/auth/register', { method: 'POST', body: JSON.stringify(payload), }), login: (payload: LoginPayload) => request('/auth/login', { method: 'POST', body: JSON.stringify(payload), }), me: () => request('/auth/me'), updateMe: (payload: UserUpdatePayload) => request('/auth/me', { method: 'PATCH', body: JSON.stringify(payload), }), forgotPassword: (email: string) => request<{ message: string; reset_token?: string }>('/auth/forgot-password', { method: 'POST', body: JSON.stringify({ email }), }), resetPassword: (token: string, new_password: string) => request<{ message: string }>('/auth/reset-password', { method: 'POST', body: JSON.stringify({ token, new_password }), }), } export function saveAuth(tokens: AuthTokens) { localStorage.setItem('silos_access_token', tokens.access_token) localStorage.setItem('silos_refresh_token', tokens.refresh_token) localStorage.setItem('silos_user', JSON.stringify(tokens.user)) } export function clearAuth() { localStorage.removeItem('silos_access_token') localStorage.removeItem('silos_refresh_token') localStorage.removeItem('silos_user') } export function getStoredUser(): User | null { const raw = localStorage.getItem('silos_user') if (!raw) return null try { return JSON.parse(raw) as User } catch { return null } } export { ApiError } export interface DocumentMember { user_id: number role: 'editor' | 'viewer' name: string email: string } export interface ApiDocument { id: string type: 'shed' | 'silo' | 'sketch' | 'stead' title: string template_id?: string | null stead_id?: string | null content: Record created_by: string created_at: string updated_at: string starred: boolean last_visited_at: string | null owner_id?: number | null visibility?: 'private' | 'members' members?: DocumentMember[] } export interface DocumentCreatePayload { type: 'shed' | 'silo' | 'sketch' | 'stead' title: string content?: Record template_id?: string stead_id?: string } export interface DocumentUpdatePayload { title?: string content?: Record starred?: boolean } export const documentsApi = { list: (params?: { type?: string stead_id?: string starred?: boolean sort?: 'updated_at' | 'last_visited_at' }) => { const search = new URLSearchParams() if (params?.type) search.set('type', params.type) if (params?.stead_id) search.set('stead_id', params.stead_id) if (params?.starred !== undefined) search.set('starred', String(params.starred)) if (params?.sort) search.set('sort', params.sort) const qs = search.toString() return request(`/documents${qs ? `?${qs}` : ''}`) }, search: (q: string) => request(`/documents/search?q=${encodeURIComponent(q)}`), get: (id: string) => request(`/documents/${id}`), create: (payload: DocumentCreatePayload) => request('/documents', { method: 'POST', body: JSON.stringify(payload), }), update: (id: string, payload: DocumentUpdatePayload) => request(`/documents/${id}`, { method: 'PATCH', body: JSON.stringify(payload), }), visit: (id: string) => request(`/documents/${id}/visit`, { method: 'POST' }), share: (id: string, email_or_username: string, role: 'editor' | 'viewer' = 'editor') => request(`/documents/${id}/share`, { method: 'POST', body: JSON.stringify({ email_or_username, role }), }), unshare: (id: string, userId: number) => request(`/documents/${id}/share/${userId}`, { method: 'DELETE' }), delete: (id: string) => request(`/documents/${id}`, { method: 'DELETE' }), }