Files
rosterchirp/frontend/src/utils/api.js

130 lines
5.5 KiB
JavaScript

const BASE = '/api';
function getToken() {
return localStorage.getItem('tc_token') || sessionStorage.getItem('tc_token');
}
// SQLite datetime('now') returns "YYYY-MM-DD HH:MM:SS" with no timezone marker.
// Browsers parse bare strings like this as LOCAL time, but the value is actually UTC.
// Appending 'Z' forces correct UTC interpretation so local display is always right.
export function parseTS(ts) {
if (!ts) return new Date(NaN);
// Already has timezone info (contains T and Z/+ or ends in Z) — leave alone
if (/Z$|[+-]\d{2}:\d{2}$/.test(ts) || (ts.includes('T') && ts.includes('Z'))) return new Date(ts);
// Replace the space separator SQLite uses and append Z
return new Date(ts.replace(' ', 'T') + 'Z');
}
async function req(method, path, body, opts = {}) {
const token = getToken();
const headers = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
let fetchOpts = { method, headers };
if (body instanceof FormData) {
fetchOpts.body = body;
} else if (body) {
headers['Content-Type'] = 'application/json';
fetchOpts.body = JSON.stringify(body);
}
const res = await fetch(BASE + path, fetchOpts);
const data = await res.json();
if (!res.ok) {
// Session displaced by a new login elsewhere — force logout
if (res.status === 401 && data.error?.includes('Session expired')) {
localStorage.removeItem('tc_token');
sessionStorage.removeItem('tc_token');
window.dispatchEvent(new CustomEvent('jama:session-displaced'));
}
throw new Error(data.error || 'Request failed');
}
return data;
}
export const api = {
// Auth
login: (body) => req('POST', '/auth/login', body),
submitSupport: (body) => req('POST', '/auth/support', body),
logout: () => req('POST', '/auth/logout'),
me: () => req('GET', '/auth/me'),
changePassword: (body) => req('POST', '/auth/change-password', body),
// Users
getUsers: () => req('GET', '/users'),
searchUsers: (q, groupId) => req('GET', `/users/search?q=${encodeURIComponent(q)}${groupId ? `&groupId=${groupId}` : ''}`),
createUser: (body) => req('POST', '/users', body),
bulkUsers: (users) => req('POST', '/users/bulk', { users }),
updateName: (id, name) => req('PATCH', `/users/${id}/name`, { name }),
updateRole: (id, role) => req('PATCH', `/users/${id}/role`, { role }),
resetPassword: (id, password) => req('PATCH', `/users/${id}/reset-password`, { password }),
suspendUser: (id) => req('PATCH', `/users/${id}/suspend`),
activateUser: (id) => req('PATCH', `/users/${id}/activate`),
deleteUser: (id) => req('DELETE', `/users/${id}`),
checkDisplayName: (name) => req('GET', `/users/check-display-name?name=${encodeURIComponent(name)}`),
updateProfile: (body) => req('PATCH', '/users/me/profile', body), // body: { displayName, aboutMe, hideAdminTag, allowDm }
uploadAvatar: (file) => {
const form = new FormData(); form.append('avatar', file);
return req('POST', '/users/me/avatar', form);
},
// Groups
getGroups: () => req('GET', '/groups'),
createGroup: (body) => req('POST', '/groups', body),
renameGroup: (id, name) => req('PATCH', `/groups/${id}/rename`, { name }),
setCustomGroupName: (id, name) => req('PATCH', `/groups/${id}/custom-name`, { name }),
getHelp: () => req('GET', '/help'),
getHelpStatus: () => req('GET', '/help/status'),
dismissHelp: (dismissed) => req('POST', '/help/dismiss', { dismissed }),
getMembers: (id) => req('GET', `/groups/${id}/members`),
addMember: (groupId, userId) => req('POST', `/groups/${groupId}/members`, { userId }),
removeMember: (groupId, userId) => req('DELETE', `/groups/${groupId}/members/${userId}`),
leaveGroup: (id) => req('DELETE', `/groups/${id}/leave`),
takeOwnership: (id) => req('POST', `/groups/${id}/take-ownership`),
deleteGroup: (id) => req('DELETE', `/groups/${id}`),
// Messages
getMessages: (groupId, before) => req('GET', `/messages/group/${groupId}${before ? `?before=${before}` : ''}`),
sendMessage: (groupId, body) => req('POST', `/messages/group/${groupId}`, body),
uploadImage: (groupId, file, extra = {}) => {
const form = new FormData();
form.append('image', file);
if (extra.replyToId) form.append('replyToId', extra.replyToId);
if (extra.content) form.append('content', extra.content);
return req('POST', `/messages/group/${groupId}/image`, form);
},
deleteMessage: (id) => req('DELETE', `/messages/${id}`),
toggleReaction: (id, emoji) => req('POST', `/messages/${id}/reactions`, { emoji }),
// Settings
getSettings: () => req('GET', '/settings'),
updateAppName: (name) => req('PATCH', '/settings/app-name', { name }),
uploadLogo: (file) => {
const form = new FormData(); form.append('logo', file);
return req('POST', '/settings/logo', form);
},
uploadIconNewChat: (file) => {
const form = new FormData(); form.append('icon', file);
return req('POST', '/settings/icon-newchat', form);
},
uploadIconGroupInfo: (file) => {
const form = new FormData(); form.append('icon', file);
return req('POST', '/settings/icon-groupinfo', form);
},
resetSettings: () => req('POST', '/settings/reset'),
// Push notifications
getPushKey: () => req('GET', '/push/vapid-public'),
subscribePush: (sub) => req('POST', '/push/subscribe', sub),
unsubscribePush: (endpoint) => req('POST', '/push/unsubscribe', { endpoint }),
// Link preview
getLinkPreview: (url) => req('GET', `/link-preview?url=${encodeURIComponent(url)}`),
// VAPID key management (admin only)
generateVapidKeys: () => req('POST', '/push/generate-vapid'),
};