diff --git a/ui/suite/drive/drive.js b/ui/suite/drive/drive.js
index dd9d43e..9b2ef09 100644
--- a/ui/suite/drive/drive.js
+++ b/ui/suite/drive/drive.js
@@ -223,8 +223,28 @@
async function apiRequest(endpoint, options = {}) {
const url = `${API_BASE}${endpoint}`;
+
+ // Use global ApiClient if available for automatic auth token injection
+ if (window.ApiClient) {
+ try {
+ return await window.ApiClient.request(url, options);
+ } catch (err) {
+ console.error(`API Error [${endpoint}]:`, err);
+ throw err;
+ }
+ }
+
+ // Fallback if ApiClient not loaded
const defaultHeaders = { "Content-Type": "application/json" };
+ // Try to get auth token from storage
+ const token =
+ localStorage.getItem("gb-access-token") ||
+ sessionStorage.getItem("gb-access-token");
+ if (token) {
+ defaultHeaders["Authorization"] = `Bearer ${token}`;
+ }
+
try {
const response = await fetch(url, {
headers: { ...defaultHeaders, ...options.headers },
diff --git a/ui/suite/index.html b/ui/suite/index.html
index 9f8000b..cb6c37b 100644
--- a/ui/suite/index.html
+++ b/ui/suite/index.html
@@ -2359,6 +2359,7 @@
+
diff --git a/ui/suite/js/api-client.js b/ui/suite/js/api-client.js
new file mode 100644
index 0000000..3ab9f17
--- /dev/null
+++ b/ui/suite/js/api-client.js
@@ -0,0 +1,394 @@
+/**
+ * API Client - Centralized API request handler with authentication
+ *
+ * This module provides a consistent way to make API requests with:
+ * - Automatic auth token injection
+ * - Request/response logging in development
+ * - Error handling
+ * - Retry logic for failed requests
+ */
+
+(function () {
+ "use strict";
+
+ const AUTH_TOKEN_KEY = "gb-access-token";
+ const REFRESH_TOKEN_KEY = "gb-refresh-token";
+ const SESSION_ID_KEY = "gb-session-id";
+
+ /**
+ * Get the current auth token from storage
+ */
+ function getAuthToken() {
+ // Try localStorage first (persistent login)
+ let token = localStorage.getItem(AUTH_TOKEN_KEY);
+ if (token) return token;
+
+ // Fall back to sessionStorage
+ token = sessionStorage.getItem(AUTH_TOKEN_KEY);
+ if (token) return token;
+
+ // Try to get from cookie
+ const cookies = document.cookie.split(";");
+ for (const cookie of cookies) {
+ const [name, value] = cookie.trim().split("=");
+ if (name === "gb_session" || name === "session_id") {
+ return value;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Set the auth token in storage
+ */
+ function setAuthToken(token, persistent = true) {
+ if (persistent) {
+ localStorage.setItem(AUTH_TOKEN_KEY, token);
+ } else {
+ sessionStorage.setItem(AUTH_TOKEN_KEY, token);
+ }
+ }
+
+ /**
+ * Clear the auth token from storage
+ */
+ function clearAuthToken() {
+ localStorage.removeItem(AUTH_TOKEN_KEY);
+ sessionStorage.removeItem(AUTH_TOKEN_KEY);
+ localStorage.removeItem(REFRESH_TOKEN_KEY);
+ sessionStorage.removeItem(REFRESH_TOKEN_KEY);
+ localStorage.removeItem(SESSION_ID_KEY);
+ sessionStorage.removeItem(SESSION_ID_KEY);
+ localStorage.removeItem("gb-token-expires");
+ sessionStorage.removeItem("gb-token-expires");
+ }
+
+ /**
+ * Get session ID
+ */
+ function getSessionId() {
+ return (
+ localStorage.getItem(SESSION_ID_KEY) ||
+ sessionStorage.getItem(SESSION_ID_KEY)
+ );
+ }
+
+ /**
+ * Set session ID
+ */
+ function setSessionId(sessionId, persistent = true) {
+ if (persistent) {
+ localStorage.setItem(SESSION_ID_KEY, sessionId);
+ } else {
+ sessionStorage.setItem(SESSION_ID_KEY, sessionId);
+ }
+ }
+
+ /**
+ * Build headers with auth token
+ */
+ function buildHeaders(customHeaders = {}) {
+ const headers = {
+ "Content-Type": "application/json",
+ ...customHeaders,
+ };
+
+ const token = getAuthToken();
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`;
+ }
+
+ const sessionId = getSessionId();
+ if (sessionId) {
+ headers["X-Session-ID"] = sessionId;
+ }
+
+ return headers;
+ }
+
+ /**
+ * Make an API request with auth
+ */
+ async function request(url, options = {}) {
+ const {
+ method = "GET",
+ body = null,
+ headers = {},
+ retries = 0,
+ retryDelay = 1000,
+ timeout = 30000,
+ credentials = "same-origin",
+ skipAuth = false,
+ } = options;
+
+ const requestHeaders = skipAuth
+ ? { "Content-Type": "application/json", ...headers }
+ : buildHeaders(headers);
+
+ const fetchOptions = {
+ method,
+ headers: requestHeaders,
+ credentials,
+ };
+
+ if (body && method !== "GET" && method !== "HEAD") {
+ fetchOptions.body =
+ typeof body === "string" ? body : JSON.stringify(body);
+ }
+
+ // Add timeout support
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
+ fetchOptions.signal = controller.signal;
+
+ let lastError;
+ let attempt = 0;
+
+ while (attempt <= retries) {
+ try {
+ const response = await fetch(url, fetchOptions);
+ clearTimeout(timeoutId);
+
+ // Handle auth errors
+ if (response.status === 401) {
+ const error = new Error("Unauthorized");
+ error.status = 401;
+ error.response = response;
+
+ // Dispatch event for global handling
+ window.dispatchEvent(
+ new CustomEvent("api:unauthorized", { detail: { url, response } }),
+ );
+
+ throw error;
+ }
+
+ if (response.status === 403) {
+ const error = new Error("Forbidden");
+ error.status = 403;
+ error.response = response;
+ throw error;
+ }
+
+ if (!response.ok) {
+ let errorBody;
+ try {
+ errorBody = await response.json();
+ } catch {
+ errorBody = { error: response.statusText };
+ }
+ const error = new Error(
+ errorBody.message || errorBody.error || "Request failed",
+ );
+ error.status = response.status;
+ error.body = errorBody;
+ error.response = response;
+ throw error;
+ }
+
+ // Handle empty responses
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.includes("application/json")) {
+ return await response.json();
+ } else if (response.status === 204) {
+ return null;
+ } else {
+ return await response.text();
+ }
+ } catch (err) {
+ clearTimeout(timeoutId);
+ lastError = err;
+
+ // Don't retry on auth errors or client errors
+ if (err.status && err.status >= 400 && err.status < 500) {
+ throw err;
+ }
+
+ // Don't retry on abort
+ if (err.name === "AbortError") {
+ const error = new Error("Request timeout");
+ error.status = 408;
+ throw error;
+ }
+
+ attempt++;
+ if (attempt <= retries) {
+ await new Promise((resolve) =>
+ setTimeout(resolve, retryDelay * attempt),
+ );
+ }
+ }
+ }
+
+ throw lastError;
+ }
+
+ /**
+ * GET request helper
+ */
+ async function get(url, options = {}) {
+ return request(url, { ...options, method: "GET" });
+ }
+
+ /**
+ * POST request helper
+ */
+ async function post(url, body, options = {}) {
+ return request(url, { ...options, method: "POST", body });
+ }
+
+ /**
+ * PUT request helper
+ */
+ async function put(url, body, options = {}) {
+ return request(url, { ...options, method: "PUT", body });
+ }
+
+ /**
+ * PATCH request helper
+ */
+ async function patch(url, body, options = {}) {
+ return request(url, { ...options, method: "PATCH", body });
+ }
+
+ /**
+ * DELETE request helper
+ */
+ async function del(url, options = {}) {
+ return request(url, { ...options, method: "DELETE" });
+ }
+
+ /**
+ * Upload file with progress tracking
+ */
+ async function upload(url, file, options = {}) {
+ const { onProgress, ...restOptions } = options;
+
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+
+ xhr.open("POST", url);
+
+ // Set auth header
+ const token = getAuthToken();
+ if (token) {
+ xhr.setRequestHeader("Authorization", `Bearer ${token}`);
+ }
+
+ const sessionId = getSessionId();
+ if (sessionId) {
+ xhr.setRequestHeader("X-Session-ID", sessionId);
+ }
+
+ // Set custom headers
+ if (restOptions.headers) {
+ Object.entries(restOptions.headers).forEach(([key, value]) => {
+ xhr.setRequestHeader(key, value);
+ });
+ }
+
+ // Track progress
+ if (onProgress && xhr.upload) {
+ xhr.upload.onprogress = (event) => {
+ if (event.lengthComputable) {
+ const progress = Math.round((event.loaded / event.total) * 100);
+ onProgress(progress, event.loaded, event.total);
+ }
+ };
+ }
+
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ try {
+ resolve(JSON.parse(xhr.responseText));
+ } catch {
+ resolve(xhr.responseText);
+ }
+ } else if (xhr.status === 401) {
+ window.dispatchEvent(
+ new CustomEvent("api:unauthorized", { detail: { url } }),
+ );
+ reject(new Error("Unauthorized"));
+ } else {
+ reject(new Error(xhr.statusText || "Upload failed"));
+ }
+ };
+
+ xhr.onerror = () => reject(new Error("Network error"));
+ xhr.ontimeout = () => reject(new Error("Upload timeout"));
+
+ // Create FormData
+ const formData = new FormData();
+ if (file instanceof File) {
+ formData.append("file", file);
+ } else if (file instanceof FormData) {
+ // If already FormData, use it directly
+ xhr.send(file);
+ return;
+ } else {
+ formData.append("file", file);
+ }
+
+ xhr.send(formData);
+ });
+ }
+
+ /**
+ * Initialize auth from login response
+ */
+ function initFromLoginResponse(response) {
+ if (response.access_token) {
+ setAuthToken(response.access_token, response.remember !== false);
+ }
+ if (response.session_id) {
+ setSessionId(response.session_id, response.remember !== false);
+ }
+ }
+
+ /**
+ * Check if user is authenticated (has token)
+ */
+ function isAuthenticated() {
+ return !!getAuthToken();
+ }
+
+ // Global handler for unauthorized responses
+ window.addEventListener("api:unauthorized", () => {
+ // Clear invalid tokens
+ clearAuthToken();
+
+ // Optionally redirect to login
+ // Only redirect if not already on login page
+ if (
+ !window.location.hash.includes("login") &&
+ !window.location.pathname.includes("/auth/")
+ ) {
+ console.warn("Session expired. Please log in again.");
+ // Could dispatch event for UI to handle
+ window.dispatchEvent(new CustomEvent("auth:expired"));
+ }
+ });
+
+ // Export API client
+ window.ApiClient = {
+ request,
+ get,
+ post,
+ put,
+ patch,
+ delete: del,
+ upload,
+ getAuthToken,
+ setAuthToken,
+ clearAuthToken,
+ getSessionId,
+ setSessionId,
+ buildHeaders,
+ initFromLoginResponse,
+ isAuthenticated,
+ };
+
+ // Also export as gbApi for backwards compatibility
+ window.gbApi = window.ApiClient;
+})();