Commit completed Torbat Pages admin frontend module, BFF proxy, routes, and validation artifacts for official platform deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
451 lines
17 KiB
TypeScript
451 lines
17 KiB
TypeScript
/**
|
|
* 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<T>(path: string, options: RequestOptions): Promise<T> {
|
|
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<T>;
|
|
}
|
|
|
|
function qs(params?: Record<string, string | number | boolean | undefined | null>) {
|
|
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<T = Record<string, unknown>>(
|
|
data: T[] | PaginatedList<T> | { 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<Record<string, unknown>[] | PaginatedList<Record<string, unknown>>>(
|
|
`${basePath}${qs(params)}`,
|
|
{ tenantId }
|
|
).then(normalizeList),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`${basePath}/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(basePath, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`${basePath}/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
remove: softDelete
|
|
? (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`${basePath}/${id}/delete`, {
|
|
tenantId,
|
|
method: "POST",
|
|
})
|
|
: (tenantId: string, id: string) =>
|
|
request<void>(`${basePath}/${id}`, { tenantId, method: "DELETE" }),
|
|
};
|
|
}
|
|
|
|
function httpDeleteResource(basePath: string) {
|
|
return {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[] | PaginatedList<Record<string, unknown>>>(
|
|
`${basePath}${qs(params)}`,
|
|
{ tenantId }
|
|
).then(normalizeList),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`${basePath}/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(basePath, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`${basePath}/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
remove: (tenantId: string, id: string) =>
|
|
request<void>(`${basePath}/${id}`, { tenantId, method: "DELETE" }),
|
|
};
|
|
}
|
|
|
|
export const experienceApi = {
|
|
health: (): Promise<HealthResponse> =>
|
|
fetch(`${EXPERIENCE_BASE}/health`).then((r) => r.json()),
|
|
|
|
capabilities: (): Promise<CapabilitiesResponse> =>
|
|
fetch(`${EXPERIENCE_BASE}/capabilities`).then((r) => r.json()),
|
|
|
|
metrics: (): Promise<MetricsResponse> =>
|
|
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<Record<string, unknown>[]>(`/api/v1/settings${qs(params)}`, { tenantId }).then(
|
|
normalizeList
|
|
),
|
|
upsert: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/settings`, {
|
|
tenantId,
|
|
method: "PUT",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
audit: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/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<string, unknown> = {}) =>
|
|
request<Record<string, unknown>>(`/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<Record<string, unknown>[]>(`/api/v1/site-theme-bindings${qs(params)}`, {
|
|
tenantId,
|
|
}).then(normalizeList),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/site-theme-bindings`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
unassign: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/site-theme-bindings/${id}/unassign`, {
|
|
tenantId,
|
|
method: "POST",
|
|
}),
|
|
},
|
|
pageLayoutBindings: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/api/v1/page-layout-bindings${qs(params)}`, {
|
|
tenantId,
|
|
}).then(normalizeList),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/page-layout-bindings`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
unassign: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/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<Record<string, unknown>[]>(`/api/v1/template-instantiations${qs(params)}`, {
|
|
tenantId,
|
|
}).then(normalizeList),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/template-instantiations/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/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<Record<string, unknown>[]>(`/api/v1/form-submissions${qs(params)}`, {
|
|
tenantId,
|
|
}).then(normalizeList),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/form-submissions/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/form-submissions`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/form-submissions/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
surveys: httpDeleteResource("/api/v1/surveys"),
|
|
surveyResponses: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/api/v1/survey-responses${qs(params)}`, {
|
|
tenantId,
|
|
}).then(normalizeList),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/survey-responses/${id}`, { tenantId }),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/survey-responses/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
|
|
publishReleases: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/api/v1/publish-releases${qs(params)}`, {
|
|
tenantId,
|
|
}).then(normalizeList),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/publish-releases/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/publish-releases`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/publish-releases/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
domainEdgeBindings: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/api/v1/domain-edge-bindings${qs(params)}`, {
|
|
tenantId,
|
|
}).then(normalizeList),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/domain-edge-bindings/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/domain-edge-bindings`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/domain-edge-bindings/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
seoMetadata: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/api/v1/seo-metadata${qs(params)}`, { tenantId }).then(
|
|
normalizeList
|
|
),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/seo-metadata/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/seo-metadata`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/seo-metadata/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
sitemapShells: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/api/v1/sitemap-shells${qs(params)}`, { tenantId }).then(
|
|
normalizeList
|
|
),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/sitemap-shells/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/sitemap-shells`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/sitemap-shells/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
robotsShells: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/api/v1/robots-shells${qs(params)}`, { tenantId }).then(
|
|
normalizeList
|
|
),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/robots-shells/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/robots-shells`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/robots-shells/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
},
|
|
pwaManifests: {
|
|
list: (tenantId: string, params?: ListParams) =>
|
|
request<Record<string, unknown>[]>(`/api/v1/pwa-manifests${qs(params)}`, { tenantId }).then(
|
|
normalizeList
|
|
),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/pwa-manifests/${id}`, { tenantId }),
|
|
create: (tenantId: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/pwa-manifests`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/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<Record<string, unknown>[]>(`/api/v1/analytics-snapshots${qs(params)}`, {
|
|
tenantId,
|
|
}).then(normalizeList),
|
|
get: (tenantId: string, id: string) =>
|
|
request<Record<string, unknown>>(`/api/v1/analytics-snapshots/${id}`, { tenantId }),
|
|
refresh: (tenantId: string, body: Record<string, unknown> = {}) =>
|
|
request<Record<string, unknown>>(`/api/v1/analytics-snapshots/refresh`, {
|
|
tenantId,
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
|
request<Record<string, unknown>>(`/api/v1/analytics-snapshots/${id}`, {
|
|
tenantId,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
remove: (tenantId: string, id: string) =>
|
|
request<void>(`/api/v1/analytics-snapshots/${id}`, { tenantId, method: "DELETE" }),
|
|
},
|
|
|
|
widgetDefinitions: httpDeleteResource("/api/v1/widget-definitions"),
|
|
widgetInstances: httpDeleteResource("/api/v1/widget-instances"),
|
|
};
|