Newer
Older
gnexus-creds / frontend / src / api.js
const jsonHeaders = { "Content-Type": "application/json", Accept: "application/json" };

async function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function requestWithRetry(path, options = {}, retries = 3) {
  let lastError;
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const response = await fetch(path, {
        credentials: "include",
        ...options,
        headers: {
          ...jsonHeaders,
          ...(options.headers || {})
        }
      });
      if (response.status === 204) {
        return null;
      }
      const payload = await response.json();
      if (!response.ok) {
        throw new Error(payload?.error?.message || "Request failed");
      }
      return payload;
    } catch (err) {
      lastError = err;
      const isNetworkError = !err.message?.includes("Request failed");
      if (!isNetworkError || attempt >= retries - 1) {
        throw err;
      }
      await sleep(300 * Math.pow(2, attempt));
    }
  }
  throw lastError;
}

function request(path, options = {}) {
  return requestWithRetry(path, options, 3);
}

export const api = {
  health: () => request("/health"),
  ready: () => request("/ready"),
  me: () => request("/api/v1/me"),
  updateMe: (payload) =>
    request("/api/v1/me", { method: "PATCH", body: JSON.stringify(payload) }),
  logout: () => request("/auth/logout", { method: "POST" }),
  listSecrets: (params = {}) => {
    const query = new URLSearchParams(params);
    return request(`/api/v1/secrets?${query}`);
  },
  createSecret: (payload) =>
    request("/api/v1/secrets", { method: "POST", body: JSON.stringify(payload) }),
  getSecret: (id) => request(`/api/v1/secrets/${id}`),
  updateSecret: (id, payload) =>
    request(`/api/v1/secrets/${id}`, { method: "PATCH", body: JSON.stringify(payload) }),
  deleteSecret: (id) => request(`/api/v1/secrets/${id}`, { method: "DELETE" }),
  revealSecret: (id) => request(`/api/v1/secrets/${id}/reveal`, { method: "POST" }),
  versions: (id) => request(`/api/v1/secrets/${id}/versions`),
  revealVersion: (id, versionId) =>
    request(`/api/v1/secrets/${id}/versions/${versionId}/reveal`, { method: "POST" }),
  audit: (params = {}) => {
    const query = new URLSearchParams(params);
    return request(`/api/v1/audit-events?${query}`);
  },
  secretAudit: (id, params = {}) => {
    const query = new URLSearchParams(params);
    return request(`/api/v1/secrets/${id}/audit-events?${query}`);
  },
  tokens: () => request("/api/v1/api-tokens"),
  createToken: (payload) =>
    request("/api/v1/api-tokens", { method: "POST", body: JSON.stringify(payload) }),
  revokeToken: (id) =>
    request(`/api/v1/api-tokens/${id}/revoke`, { method: "PATCH" }),
  exportData: () => request("/api/v1/export", { method: "POST" }),
  importData: (payload) =>
    request("/api/v1/import", { method: "POST", body: JSON.stringify(payload) }),
  deleteAccountData: () => request("/api/v1/account-data", { method: "DELETE" }),
  adminUsers: (params = {}) => {
    const query = new URLSearchParams(params);
    return request(`/api/v1/admin/users?${query}`);
  },
  stats: () => request("/api/v1/stats"),
  createBackup: () => request("/api/v1/admin/backup", { method: "POST" }),
  listBackups: () => request("/api/v1/admin/backups"),
  downloadBackup: (filename) => `/api/v1/admin/backups/${encodeURIComponent(filename)}`,
  restoreBackup: (filename) =>
    request("/api/v1/admin/restore", { method: "POST", body: JSON.stringify({ filename }) })
};