Files
honeyDueWeb/src/lib/api/client.ts
T
Trey t e2172c20f2 Rebrand from Casera/MyCrib to honeyDue
Total rebrand across Web project:
- Package name: casera-web -> honeydue-web
- Cookie: casera-token -> honeydue-token
- Theme store: casera-theme -> honeydue-theme
- File sharing: .casera -> .honeydue, component/function renames
- casera-file-handler.tsx -> honeydue-file-handler.tsx
- All UI text, metadata, OG tags updated
- Domains: casera.treytartt.com -> honeyDue.treytartt.com
- Demo data emails updated
- All documentation updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:59 -06:00

144 lines
4.7 KiB
TypeScript

// ---------------------------------------------------------------------------
// Base API client for honeyDue web app
// ---------------------------------------------------------------------------
// All client-side requests go through Next.js API route handlers (proxy).
// The proxy reads the httpOnly `honeydue-token` cookie and forwards it to the
// Go API as an Authorization header. This avoids exposing the token to JS.
// ---------------------------------------------------------------------------
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_URL || 'https://honeyDue.treytartt.com/api';
/**
* Server-only base URL. Falls back to the public one so that server
* components / route handlers can reach the Go API directly.
*/
const SERVER_API_URL = process.env.API_URL || API_BASE_URL;
// ---------------------------------------------------------------------------
// Error class
// ---------------------------------------------------------------------------
export class ApiError extends Error {
constructor(
public status: number,
message: string,
public details?: Record<string, string[]>,
) {
super(message);
this.name = 'ApiError';
}
}
// ---------------------------------------------------------------------------
// Client-side fetcher (calls Next.js /api/proxy/... routes)
// ---------------------------------------------------------------------------
/**
* Client-side authenticated fetch. Calls the Next.js catch-all proxy which
* attaches the auth token from the httpOnly cookie before forwarding to Go.
*
* @param path API path *without* the `/api` prefix, e.g. `/tasks/`
* @param options Standard RequestInit overrides
*/
export async function apiFetch<T>(
path: string,
options: RequestInit = {},
): Promise<T> {
// Strip trailing slash for the Next.js proxy URL (Next.js 308-redirects
// trailing slashes away by default). The proxy route handler re-adds the
// trailing slash when forwarding to the Go API.
const normalized = path.endsWith('/') ? path.slice(0, -1) : path;
const url = `/api/proxy${normalized}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Timezone': Intl.DateTimeFormat().resolvedOptions().timeZone,
...(options.headers as Record<string, string>),
};
// If the caller explicitly set Content-Type to something else (e.g. for
// FormData uploads), remove our default so the browser can set the
// multipart boundary automatically.
if (options.body instanceof FormData) {
delete headers['Content-Type'];
}
const res = await fetch(url, { ...options, headers });
if (!res.ok) {
const body = await res.json().catch(() => ({ error: 'Unknown error' }));
throw new ApiError(
res.status,
body.message || body.error || 'Request failed',
body.details,
);
}
// Handle 204 No Content
if (res.status === 204) return undefined as T;
return res.json();
}
// ---------------------------------------------------------------------------
// Server-side fetcher (for server components & route handlers)
// ---------------------------------------------------------------------------
/**
* Server-side fetch that reads the auth token from the `honeydue-token` cookie
* and calls the Go API directly (no proxy hop).
*
* Only use this inside:
* - `app/api/.../route.ts` handlers
* - Server Components
* - Server Actions
*/
export async function serverFetch<T>(
path: string,
options: RequestInit = {},
): Promise<T> {
// Dynamic import so this module can still be imported from client bundles
// (the function itself should only be *called* on the server).
const { cookies } = await import('next/headers');
const cookieStore = await cookies();
const token = cookieStore.get('honeydue-token')?.value;
const normalized = path.endsWith('/') ? path : `${path}/`;
const url = `${SERVER_API_URL}${normalized}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Timezone': 'UTC', // Server-side default; client overrides via proxy
...(options.headers as Record<string, string>),
};
if (token) {
headers['Authorization'] = `Token ${token}`;
}
if (options.body instanceof FormData) {
delete headers['Content-Type'];
}
const res = await fetch(url, {
...options,
headers,
// Disable Next.js fetch cache for mutations; callers can override.
cache: options.cache ?? 'no-store',
});
if (!res.ok) {
const body = await res.json().catch(() => ({ error: 'Unknown error' }));
throw new ApiError(
res.status,
body.message || body.error || 'Request failed',
body.details,
);
}
if (res.status === 204) return undefined as T;
return res.json();
}