/** * Experience Platform API client — Torbat Pages (real backend only). * Browser → /api/experience BFF; SSR → direct upstream :8008 */ import { getValidAccessToken, refreshSession } from "@/lib/auth"; import type { CapabilitiesResponse, HealthResponse, ListParams, MetricsResponse, PaginatedList, } from "@/modules/experience/types"; const EXPERIENCE_BASE = typeof window !== "undefined" ? "/api/experience" : process.env.EXPERIENCE_SERVICE_URL || process.env.NEXT_PUBLIC_EXPERIENCE_API_URL || "http://localhost:8008"; export class ExperienceApiError extends Error { constructor( public status: number, public code: string, message: string ) { super(message); this.name = "ExperienceApiError"; } } type RequestOptions = RequestInit & { tenantId: string; auth?: boolean }; async function request(path: string, options: RequestOptions): Promise { const { tenantId, auth = true, headers: customHeaders, ...init } = options; const headers = new Headers(customHeaders); if (!headers.has("Content-Type") && init.body) { headers.set("Content-Type", "application/json"); } headers.set("X-Tenant-ID", tenantId); if (auth) { const token = await getValidAccessToken(); if (token) headers.set("Authorization", `Bearer ${token}`); } const url = path.startsWith("http") ? path : `${EXPERIENCE_BASE.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`; let res: Response; try { res = await fetch(url, { ...init, headers }); } catch (err) { const detail = err instanceof Error ? err.message : "network_error"; throw new Error(`اتصال به سرویس تربت پیجز برقرار نشد (${url}) — ${detail}`); } if (res.status === 401 && auth) { const refreshed = await refreshSession({ force: true }); if (refreshed) { headers.set("Authorization", `Bearer ${refreshed}`); res = await fetch(url, { ...init, headers }); } } if (!res.ok) { const body = await res.json().catch(() => ({})); throw new ExperienceApiError( res.status, body?.error?.code || body?.detail || "unknown_error", body?.error?.message || body?.detail || res.statusText ); } if (res.status === 204) return undefined as T; return res.json() as Promise; } function qs(params?: Record) { if (!params) return ""; const sp = new URLSearchParams(); Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== "") sp.set(k, String(v)); }); const s = sp.toString(); return s ? `?${s}` : ""; } export function normalizeList>( data: T[] | PaginatedList | { items?: T[] } ): T[] { if (Array.isArray(data)) return data; if ("items" in data && Array.isArray(data.items)) return data.items; return []; } function crudResource(basePath: string, softDelete = true) { return { list: (tenantId: string, params?: ListParams) => request[] | PaginatedList>>( `${basePath}${qs(params)}`, { tenantId } ).then(normalizeList), get: (tenantId: string, id: string) => request>(`${basePath}/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(basePath, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`${basePath}/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), remove: softDelete ? (tenantId: string, id: string) => request>(`${basePath}/${id}/delete`, { tenantId, method: "POST", }) : (tenantId: string, id: string) => request(`${basePath}/${id}`, { tenantId, method: "DELETE" }), }; } function httpDeleteResource(basePath: string) { return { list: (tenantId: string, params?: ListParams) => request[] | PaginatedList>>( `${basePath}${qs(params)}`, { tenantId } ).then(normalizeList), get: (tenantId: string, id: string) => request>(`${basePath}/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(basePath, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`${basePath}/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), remove: (tenantId: string, id: string) => request(`${basePath}/${id}`, { tenantId, method: "DELETE" }), }; } export const experienceApi = { health: (): Promise => fetch(`${EXPERIENCE_BASE}/health`).then((r) => r.json()), capabilities: (): Promise => fetch(`${EXPERIENCE_BASE}/capabilities`).then((r) => r.json()), metrics: (): Promise => fetch(`${EXPERIENCE_BASE}/metrics`).then((r) => r.json()), workspaces: crudResource("/api/v1/workspaces"), localeProfiles: crudResource("/api/v1/locale-profiles"), externalProviders: crudResource("/api/v1/external-providers"), renderEngines: crudResource("/api/v1/render-engines"), configurations: crudResource("/api/v1/configurations"), settings: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/settings${qs(params)}`, { tenantId }).then( normalizeList ), upsert: (tenantId: string, body: Record) => request>(`/api/v1/settings`, { tenantId, method: "PUT", body: JSON.stringify(body), }), }, audit: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/audit${qs(params)}`, { tenantId }).then( normalizeList ), }, sites: crudResource("/api/v1/sites"), pages: crudResource("/api/v1/pages"), siteDomains: crudResource("/api/v1/site-domains"), components: crudResource("/api/v1/components"), componentVersions: { ...crudResource("/api/v1/component-versions", false), publish: (tenantId: string, id: string, body: Record = {}) => request>(`/api/v1/component-versions/${id}/publish`, { tenantId, method: "POST", body: JSON.stringify(body), }), }, pageComponentPlacements: crudResource("/api/v1/page-component-placements"), themes: crudResource("/api/v1/themes"), themeVersions: crudResource("/api/v1/theme-versions", false), layouts: crudResource("/api/v1/layouts"), layoutVersions: crudResource("/api/v1/layout-versions", false), siteThemeBindings: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/site-theme-bindings${qs(params)}`, { tenantId, }).then(normalizeList), create: (tenantId: string, body: Record) => request>(`/api/v1/site-theme-bindings`, { tenantId, method: "POST", body: JSON.stringify(body), }), unassign: (tenantId: string, id: string) => request>(`/api/v1/site-theme-bindings/${id}/unassign`, { tenantId, method: "POST", }), }, pageLayoutBindings: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/page-layout-bindings${qs(params)}`, { tenantId, }).then(normalizeList), create: (tenantId: string, body: Record) => request>(`/api/v1/page-layout-bindings`, { tenantId, method: "POST", body: JSON.stringify(body), }), unassign: (tenantId: string, id: string) => request>(`/api/v1/page-layout-bindings/${id}/unassign`, { tenantId, method: "POST", }), }, templates: crudResource("/api/v1/templates"), templateVersions: crudResource("/api/v1/template-versions", false), templateInstantiations: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/template-instantiations${qs(params)}`, { tenantId, }).then(normalizeList), get: (tenantId: string, id: string) => request>(`/api/v1/template-instantiations/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(`/api/v1/template-instantiations`, { tenantId, method: "POST", body: JSON.stringify(body), }), }, localizations: httpDeleteResource("/api/v1/localizations"), mediaRefs: httpDeleteResource("/api/v1/media-refs"), forms: httpDeleteResource("/api/v1/forms"), formSubmissions: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/form-submissions${qs(params)}`, { tenantId, }).then(normalizeList), get: (tenantId: string, id: string) => request>(`/api/v1/form-submissions/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(`/api/v1/form-submissions`, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/form-submissions/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, surveys: httpDeleteResource("/api/v1/surveys"), surveyResponses: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/survey-responses${qs(params)}`, { tenantId, }).then(normalizeList), get: (tenantId: string, id: string) => request>(`/api/v1/survey-responses/${id}`, { tenantId }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/survey-responses/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, publishReleases: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/publish-releases${qs(params)}`, { tenantId, }).then(normalizeList), get: (tenantId: string, id: string) => request>(`/api/v1/publish-releases/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(`/api/v1/publish-releases`, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/publish-releases/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, domainEdgeBindings: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/domain-edge-bindings${qs(params)}`, { tenantId, }).then(normalizeList), get: (tenantId: string, id: string) => request>(`/api/v1/domain-edge-bindings/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(`/api/v1/domain-edge-bindings`, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/domain-edge-bindings/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, seoMetadata: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/seo-metadata${qs(params)}`, { tenantId }).then( normalizeList ), get: (tenantId: string, id: string) => request>(`/api/v1/seo-metadata/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(`/api/v1/seo-metadata`, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/seo-metadata/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, sitemapShells: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/sitemap-shells${qs(params)}`, { tenantId }).then( normalizeList ), get: (tenantId: string, id: string) => request>(`/api/v1/sitemap-shells/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(`/api/v1/sitemap-shells`, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/sitemap-shells/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, robotsShells: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/robots-shells${qs(params)}`, { tenantId }).then( normalizeList ), get: (tenantId: string, id: string) => request>(`/api/v1/robots-shells/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(`/api/v1/robots-shells`, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/robots-shells/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, pwaManifests: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/pwa-manifests${qs(params)}`, { tenantId }).then( normalizeList ), get: (tenantId: string, id: string) => request>(`/api/v1/pwa-manifests/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>(`/api/v1/pwa-manifests`, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/pwa-manifests/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, analyticsReportDefinitions: httpDeleteResource("/api/v1/analytics-report-definitions"), analyticsSnapshots: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/analytics-snapshots${qs(params)}`, { tenantId, }).then(normalizeList), get: (tenantId: string, id: string) => request>(`/api/v1/analytics-snapshots/${id}`, { tenantId }), refresh: (tenantId: string, body: Record = {}) => request>(`/api/v1/analytics-snapshots/refresh`, { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/analytics-snapshots/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), remove: (tenantId: string, id: string) => request(`/api/v1/analytics-snapshots/${id}`, { tenantId, method: "DELETE" }), }, widgetDefinitions: httpDeleteResource("/api/v1/widget-definitions"), widgetInstances: httpDeleteResource("/api/v1/widget-instances"), };