const API_BASE = '/api' async function parseResponse(response) { if (response.status === 204) { return null } const data = await response.json().catch(() => null) if (!response.ok) { const message = data?.message || data?.['hydra:description'] || response.statusText throw new Error(message) } return data } export async function apiFetch(path, options = {}) { const token = localStorage.getItem('speedbb_token') const headers = { Accept: 'application/json', ...(options.headers || {}), } if (!(options.body instanceof FormData)) { if (!headers['Content-Type']) { headers['Content-Type'] = 'application/json' } } if (token) { headers.Authorization = `Bearer ${token}` } const response = await fetch(`${API_BASE}${path}`, { ...options, headers, }) if (response.status === 401) { localStorage.removeItem('speedbb_token') localStorage.removeItem('speedbb_email') localStorage.removeItem('speedbb_user_id') localStorage.removeItem('speedbb_roles') window.dispatchEvent(new Event('speedbb-unauthorized')) } return parseResponse(response) } export async function getCollection(path) { const data = await apiFetch(path) if (Array.isArray(data)) { return data } return data?.['hydra:member'] || [] } export async function login(email, password) { return apiFetch('/login', { method: 'POST', body: JSON.stringify({ email, password }), }) } export async function registerUser({ email, username, plainPassword }) { return apiFetch('/register', { method: 'POST', body: JSON.stringify({ email, username, plainPassword }), }) } export async function listRootForums() { return getCollection('/forums?parent[exists]=false') } export async function listAllForums() { return getCollection('/forums?pagination=false') } export async function fetchVersion() { return apiFetch('/version') } export async function fetchSetting(key) { const cacheBust = Date.now() const data = await apiFetch( `/settings?key=${encodeURIComponent(key)}&pagination=false&_=${cacheBust}`, { cache: 'no-store' } ) if (Array.isArray(data)) { return data[0] || null } return data?.['hydra:member']?.[0] || null } export async function saveSetting(key, value) { return apiFetch('/settings', { method: 'POST', body: JSON.stringify({ key, value }), }) } export async function uploadLogo(file) { const body = new FormData() body.append('file', file) return apiFetch('/uploads/logo', { method: 'POST', body, }) } export async function uploadFavicon(file) { const body = new FormData() body.append('file', file) return apiFetch('/uploads/favicon', { method: 'POST', body, }) } export async function fetchUserSetting(key) { const data = await getCollection(`/user-settings?key=${encodeURIComponent(key)}&pagination=false`) return data[0] || null } export async function saveUserSetting(key, value) { return apiFetch('/user-settings', { method: 'POST', body: JSON.stringify({ key, value }), }) } export async function listForumsByParent(parentId) { return getCollection(`/forums?parent=/api/forums/${parentId}`) } export async function getForum(id) { return apiFetch(`/forums/${id}`) } export async function createForum({ name, description, type, parentId }) { return apiFetch('/forums', { method: 'POST', body: JSON.stringify({ name, description, type, parent: parentId ? `/api/forums/${parentId}` : null, }), }) } export async function updateForum(id, { name, description, type, parentId }) { return apiFetch(`/forums/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/merge-patch+json', }, body: JSON.stringify({ name, description, type, parent: parentId ? `/api/forums/${parentId}` : null, }), }) } export async function deleteForum(id) { return apiFetch(`/forums/${id}`, { method: 'DELETE', }) } export async function reorderForums(parentId, orderedIds) { return apiFetch('/forums/reorder', { method: 'POST', body: JSON.stringify({ parentId, orderedIds, }), }) } export async function listThreadsByForum(forumId) { return getCollection(`/threads?forum=/api/forums/${forumId}`) } export async function listThreads() { return getCollection('/threads') } export async function getThread(id) { return apiFetch(`/threads/${id}`) } export async function listPostsByThread(threadId) { return getCollection(`/posts?thread=/api/threads/${threadId}`) } export async function listUsers() { return getCollection('/users') } export async function createThread({ title, body, forumId }) { return apiFetch('/threads', { method: 'POST', body: JSON.stringify({ title, body, forum: `/api/forums/${forumId}`, }), }) } export async function createPost({ body, threadId }) { return apiFetch('/posts', { method: 'POST', body: JSON.stringify({ body, thread: `/api/threads/${threadId}`, }), }) }