Files
emdash-patch-imageupload/packages/admin/src/lib/api/client.ts
kunthawat 2d1be52177 Emdash source with visual editor image upload fix
Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
2026-05-03 10:44:54 +07:00

204 lines
5.4 KiB
TypeScript

/**
* Base API client configuration and shared types
*/
import type { Element } from "@emdash-cms/blocks";
export const API_BASE = "/_emdash/api";
/**
* Fetch wrapper that adds the X-EmDash-Request CSRF protection header
* to all requests. All API calls should use this instead of raw fetch().
*/
export function apiFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
const headers = new Headers(init?.headers);
headers.set("X-EmDash-Request", "1");
return fetch(input, { ...init, headers });
}
/**
* Throw an error with the message from the API response body if available,
* falling back to a generic message. All API error responses use the shape
* `{ error: { code, message } }`.
*/
export async function throwResponseError(res: Response, fallback: string): Promise<never> {
const body: unknown = await res.json().catch(() => ({}));
let message: string | undefined;
if (typeof body === "object" && body !== null && "error" in body) {
const { error } = body;
if (typeof error === "object" && error !== null && "message" in error) {
const { message: msg } = error;
if (typeof msg === "string") message = msg;
}
}
throw new Error(message || `${fallback}: ${res.statusText}`);
}
/**
* Generic paginated result
*/
export interface FindManyResult<T> {
items: T[];
nextCursor?: string;
}
/**
* Admin manifest describing available collections and plugins
*/
export interface AdminManifest {
version: string;
hash: string;
collections: Record<
string,
{
label: string;
labelSingular: string;
supports: string[];
hasSeo: boolean;
urlPattern?: string;
fields: Record<
string,
{
kind: string;
label?: string;
required?: boolean;
widget?: string;
/**
* For `select` / `multiSelect`: the list of enum choices.
* For `json` fields driven by a plugin `widget`: arbitrary widget config.
*/
options?: Array<{ value: string; label: string }> | Record<string, unknown>;
validation?: Record<string, unknown>;
}
>;
}
>;
plugins: Record<
string,
{
name?: string;
version?: string;
/** Package name for dynamic import (e.g., "@emdash-cms/plugin-audit-log") */
package?: string;
/** Whether the plugin is enabled */
enabled?: boolean;
/**
* How this plugin renders its admin UI:
* - "react": Trusted plugin with React components
* - "blocks": Declarative Block Kit UI via admin route handler
* - "none": No admin UI
*/
adminMode?: "react" | "blocks" | "none";
adminPages?: Array<{
path: string;
label?: string;
icon?: string;
}>;
dashboardWidgets?: Array<{
id: string;
title?: string;
size?: "full" | "half" | "third";
}>;
fieldWidgets?: Array<{
name: string;
label: string;
fieldTypes: string[];
elements?: import("@emdash-cms/blocks").Element[];
}>;
/** Block types for Portable Text editor */
portableTextBlocks?: Array<{
type: string;
label: string;
icon?: string;
description?: string;
placeholder?: string;
fields?: Element[];
category?: string;
}>;
}
>;
/**
* Auth mode for the admin UI. When "passkey", the security settings
* (passkey management, self-signup domains) are shown. When using
* external auth (e.g., "cloudflare-access"), these are hidden since
* authentication is handled externally.
*/
authMode: string;
/**
* Whether self-signup is enabled (at least one allowed domain is active).
* Used by the login page to conditionally show the "Sign up" link.
*/
signupEnabled?: boolean;
/**
* i18n configuration. Present when multiple locales are configured.
*/
i18n?: {
defaultLocale: string;
locales: string[];
};
/**
* Taxonomy definitions for the admin sidebar.
*/
taxonomies: Array<{
name: string;
label: string;
labelSingular?: string;
hierarchical: boolean;
collections: string[];
}>;
/**
* Marketplace registry URL. Present when `marketplace` is configured
* in the EmDash integration. Enables marketplace features in the UI.
*/
marketplace?: string;
/**
* Admin branding overrides for white-labeling.
* Set via the `admin` config in `astro.config.mjs`.
*/
admin?: {
logo?: string;
siteName?: string;
favicon?: string;
};
}
/**
* Parse an API response with the { data: T } envelope.
*
* Handles error responses via throwResponseError, then unwraps the data envelope.
* Replaces both bare `response.json()` and field-unwrap patterns.
*/
export async function parseApiResponse<T>(
response: Response,
fallbackMessage = "Request failed",
): Promise<T> {
if (!response.ok) await throwResponseError(response, fallbackMessage);
const body: { data: T } = await response.json();
return body.data;
}
/**
* Fetch admin manifest
*/
export async function fetchManifest(): Promise<AdminManifest> {
const response = await apiFetch(`${API_BASE}/manifest`);
return parseApiResponse<AdminManifest>(response, "Failed to fetch manifest");
}
/**
* Fetch auth mode (public endpoint — works without authentication).
* Used by the login page to determine which login UI to render.
*/
export async function fetchAuthMode(): Promise<{
authMode: string;
signupEnabled?: boolean;
providers?: Array<{ id: string; label: string }>;
}> {
const response = await apiFetch(`${API_BASE}/auth/mode`);
return parseApiResponse<{
authMode: string;
signupEnabled?: boolean;
providers?: Array<{ id: string; label: string }>;
}>(response, "Failed to fetch auth mode");
}