// Thin fetch wrapper. Always sends cookies (httpOnly JWT) and talks to the // same-origin API (/api/v1) — proxied to the Express server in dev. const BASE = '/api/v1' class ApiError extends Error { constructor(status, message, body) { super(message) this.status = status this.body = body } } async function req(path, { method = 'GET', body, headers, raw } = {}) { const opts = { method, credentials: 'include', headers: { ...headers } } if (body !== undefined) { if (raw) { opts.body = body // FormData — let the browser set the content-type } else { opts.headers['Content-Type'] = 'application/json' opts.body = JSON.stringify(body) } } const res = await fetch(BASE + path, opts) const text = await res.text() const data = text ? safeParse(text) : null if (!res.ok) { const message = (data && data.message) || res.statusText || 'Request failed' throw new ApiError(res.status, message, data) } return data } function safeParse(text) { try { return JSON.parse(text) } catch { return text } } export const api = { // ----- auth ----- me: () => req('/auth/me'), // `extra` carries the honeypot field (and any future login fields). login: (username, password, extra = {}) => req('/auth/login', { method: 'POST', body: { username, password, ...extra } }), loginTotp: (challenge, code) => req('/auth/login/totp', { method: 'POST', body: { challenge, code } }), logout: () => req('/auth/logout', { method: 'POST' }), // Public SSO provider discovery — drives the login-page provider buttons. authProviders: () => req('/auth/providers'), // ----- public ----- publicSettings: () => req('/public/settings'), status: () => req('/public/status'), posts: (category) => req(`/public/posts/${category}`), post: (category, idOrSlug) => req(`/public/posts/${category}/${idOrSlug}`), wiki: (opts = {}) => { const qs = new URLSearchParams() if (opts.category) qs.set('category', opts.category) if (opts.tag) qs.set('tag', opts.tag) if (opts.q) qs.set('q', opts.q) const s = qs.toString() return req(`/public/wiki${s ? `?${s}` : ''}`) }, wikiCategories: () => req('/public/wiki/categories'), wikiTags: () => req('/public/wiki/tags'), wikiPage: (slug) => req(`/public/wiki/${slug}`), contact: (payload) => req('/public/contact', { method: 'POST', body: payload }), // ----- admin ----- admin: { dashboard: () => req('/admin/dashboard'), setSiteMode: (mode) => req('/admin/site-mode', { method: 'PUT', body: { mode } }), listPosts: (category) => req(`/admin/posts${category ? `?category=${category}` : ''}`), getPost: (id) => req(`/admin/posts/${id}`), createPost: (data) => req('/admin/posts', { method: 'POST', body: data }), updatePost: (id, data) => req(`/admin/posts/${id}`, { method: 'PUT', body: data }), deletePost: (id) => req(`/admin/posts/${id}`, { method: 'DELETE' }), publishPost: (id, published) => req(`/admin/posts/${id}/publish`, { method: 'PATCH', body: { published } }), uploadImage: (file) => { const fd = new FormData() fd.append('image', file) return req('/admin/posts/upload', { method: 'POST', body: fd, raw: true }) }, // Generalized upload for rich-text editors → { url }. upload: (file) => { const fd = new FormData() fd.append('image', file) return req('/admin/uploads', { method: 'POST', body: fd, raw: true }) }, listWiki: (params = '') => req(`/admin/wiki${params}`), getWiki: (slug) => req(`/admin/wiki/${slug}`), createWiki: (data) => req('/admin/wiki', { method: 'POST', body: data }), updateWiki: (slug, data) => req(`/admin/wiki/${slug}`, { method: 'PUT', body: data }), publishWiki: (slug, published) => req(`/admin/wiki/${slug}/publish`, { method: 'PATCH', body: { published } }), deleteWiki: (slug) => req(`/admin/wiki/${slug}`, { method: 'DELETE' }), listWikiRevisions: (slug) => req(`/admin/wiki/${slug}/revisions`), getWikiRevision: (slug, id) => req(`/admin/wiki/${slug}/revisions/${id}`), restoreWikiRevision: (slug, id) => req(`/admin/wiki/${slug}/revisions/${id}/restore`, { method: 'POST' }), listWikiTags: () => req('/admin/wiki/tags'), listWikiCategories: () => req('/admin/wiki/categories'), createWikiCategory: (data) => req('/admin/wiki/categories', { method: 'POST', body: data }), updateWikiCategory: (id, data) => req(`/admin/wiki/categories/${id}`, { method: 'PUT', body: data }), deleteWikiCategory: (id) => req(`/admin/wiki/categories/${id}`, { method: 'DELETE' }), getSettings: () => req('/admin/settings'), updateSettings: (obj) => req('/admin/settings', { method: 'PUT', body: obj }), activity: (limit = 50) => req(`/admin/activity?limit=${limit}`), botActivity: () => req('/admin/bot-activity'), unbanIp: (ip) => req('/admin/bot-activity/unban', { method: 'POST', body: { ip } }), listUsers: () => req('/admin/users'), createUser: (data) => req('/admin/users', { method: 'POST', body: data }), updateUser: (id, data) => req(`/admin/users/${id}`, { method: 'PUT', body: data }), deleteUser: (id) => req(`/admin/users/${id}`, { method: 'DELETE' }), // ----- account security (self-service 2FA) ----- getAccount: () => req('/admin/account'), totpSetup: () => req('/admin/account/totp/setup', { method: 'POST' }), totpEnable: (code) => req('/admin/account/totp/enable', { method: 'POST', body: { code } }), totpDisable: (code) => req('/admin/account/totp/disable', { method: 'POST', body: { code } }), // ----- linked SSO identities (self-service) ----- linkedIdentities: () => req('/admin/account/identities'), unlinkIdentity: (provider) => req(`/admin/account/identities/${provider}`, { method: 'DELETE' }), // ----- auth providers / SSO config (admin only) ----- listAuthProviders: () => req('/admin/auth/providers'), createAuthProvider: (data) => req('/admin/auth/providers', { method: 'POST', body: data }), updateAuthProvider: (id, data) => req(`/admin/auth/providers/${id}`, { method: 'PUT', body: data }), deleteAuthProvider: (id) => req(`/admin/auth/providers/${id}`, { method: 'DELETE' }), }, } export { ApiError }