const TOKEN_KEY = 'sw-token' export function getToken() { return localStorage.getItem(TOKEN_KEY) } export function setToken(t) { localStorage.setItem(TOKEN_KEY, t) } export function clearToken() { localStorage.removeItem(TOKEN_KEY) } export function getUser() { const token = getToken() if (!token) return null try { const payload = JSON.parse(atob(token.split('.')[1])) if (payload.exp * 1000 < Date.now()) { clearToken(); return null } return payload } catch { return null } } // Support tokens that still carry `email` from before the username migration export function getUsername() { const u = getUser() return u?.username ?? u?.email ?? '' } async function req(method, url, body, extraHeaders = {}) { const token = getToken() const isForm = body instanceof FormData const headers = { ...(!isForm && body ? { 'Content-Type': 'application/json' } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}), ...extraHeaders, } const res = await fetch(url, { method, headers, body: isForm ? body : body ? JSON.stringify(body) : undefined, }) const data = await res.json().catch(() => ({})) if (!res.ok) throw Object.assign(new Error(data.error || 'Request failed'), { status: res.status }) return data } async function download(url) { const token = getToken() const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }) if (!res.ok) throw new Error('Export failed') const blob = await res.blob() const fname = res.headers.get('content-disposition')?.match(/filename="([^"]+)"/)?.[1] || 'export' const a = document.createElement('a') a.href = URL.createObjectURL(blob) a.download = fname a.click() URL.revokeObjectURL(a.href) } export const api = { login: async (username, password) => { const data = await req('POST', '/api/auth/login', { username, password }) setToken(data.token) return data.user }, logout: clearToken, changePassword: (current, newPassword) => req('PUT', '/api/auth/password', { current, newPassword }), getStories: () => req('GET', '/api/stories'), getStory: (id) => req('GET', `/api/stories/${id}`), createStory: (data) => req('POST', '/api/stories', data), updateStory: (id, data) => req('PUT', `/api/stories/${id}`, data), deleteStory: (id) => req('DELETE', `/api/stories/${id}`), uploadImage: async (file) => { const form = new FormData() form.append('file', file) const data = await req('POST', '/api/images', form) return data.url }, getNotes: (storyId) => req('GET', `/api/stories/${storyId}/notes`), createNote: (storyId) => req('POST', `/api/stories/${storyId}/notes`), updateNote: (storyId, noteId, data) => req('PUT', `/api/stories/${storyId}/notes/${noteId}`, data), deleteNote: (storyId, noteId) => req('DELETE', `/api/stories/${storyId}/notes/${noteId}`), lintCheck: (text, language = 'en-US') => req('POST', '/api/lint/check', { text, language }), getPrompt: () => req('GET', '/api/prompts'), exportEpub: (id) => download(`/api/stories/${id}/export/epub`), exportOdt: (id) => download(`/api/stories/${id}/export/odt`), admin: { getUsers: (pw) => req('GET', '/api/admin/users', null, { 'x-admin-password': pw }), createUser: (pw, data) => req('POST', '/api/admin/users', data, { 'x-admin-password': pw }), resetPassword: (pw, id, pass) => req('PUT', `/api/admin/users/${id}/password`, { password: pass }, { 'x-admin-password': pw }), deleteUser: (pw, id) => req('DELETE', `/api/admin/users/${id}`, null, { 'x-admin-password': pw }), }, }