UNPKG

scrivito

Version:

Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.

330 lines (284 loc) 8 kB
import { RequestFailedError, fetchJson } from 'scrivito_sdk/client'; import { ClientError } from 'scrivito_sdk/client/client_error'; import { getClientVersion } from 'scrivito_sdk/client/get_client_version'; import { withLoginHandler } from 'scrivito_sdk/client/login_handler'; import { loginRedirectHandler } from 'scrivito_sdk/client/login_redirect_handler'; import { PublicAuthentication } from 'scrivito_sdk/client/public_authentication'; import { Deferred, InternalError, ScrivitoError, fetchConfiguredTenant, onReset, wait, } from 'scrivito_sdk/common'; export class MissingWorkspaceError extends ScrivitoError {} export interface AuthorizationProvider { currentState?: () => string | null; authorize: ( request: (auth: string | undefined) => Promise<Response> ) => Promise<Response>; } export type BackendResponse = unknown; type ParamsType = unknown; interface SuccessfulTaskData { status: 'success'; result: unknown; } interface OpenTaskData { status: 'open'; id: string; } interface FailedTaskData { status: 'error'; message: string; code: string; } interface ExceptionTaskData { status: 'exception'; message: string; } type TaskData = | OpenTaskData | SuccessfulTaskData | FailedTaskData | ExceptionTaskData; interface TaskResponse { task: TaskData; } export interface VisitorSession { id: string; role: 'visitor'; token: string; maxage: number; } let requestsAreDisabled: true | undefined; export type Priority = 'foreground' | 'background'; let fallbackPriority: undefined | Priority; export function useDefaultPriority(priority: Priority) { fallbackPriority = priority; } export type AnalyticsData = | { loadId: number; urlPath: string; nav: number; } | { loadId: number }; type AnalyticsProvider = () => AnalyticsData; interface CmsRestApiConfig { apiBaseUrl: string; analyticsProvider?: AnalyticsProvider; accessAsEditor?: boolean; priority?: Priority; authorizationProvider: AuthorizationProvider; } class CmsRestApi { // only public for test purposes config?: CmsRestApiConfig; private configDeferred: Deferred<CmsRestApiConfig>; constructor() { this.configDeferred = new Deferred(); } init({ apiBaseUrl, authorizationProvider, priority, accessAsEditor, analyticsProvider, }: { apiBaseUrl: string; authorizationProvider?: AuthorizationProvider; priority?: Priority; accessAsEditor?: boolean; analyticsProvider?: AnalyticsProvider; }): void { this.config = { apiBaseUrl, analyticsProvider, accessAsEditor, priority, authorizationProvider: authorizationProvider ?? PublicAuthentication, }; this.configDeferred.resolve(this.config); } rejectRequests(): void { requestsAreDisabled = true; } async get( path: string, requestParams?: ParamsType ): Promise<BackendResponse> { return this.requestWithTaskHandling({ method: 'GET', path, requestParams }); } async put( path: string, requestParams?: ParamsType ): Promise<BackendResponse> { return this.requestWithTaskHandling({ method: 'PUT', path, requestParams }); } async post( path: string, requestParams?: ParamsType ): Promise<BackendResponse> { return this.requestWithTaskHandling({ method: 'POST', path, requestParams, }); } async delete( path: string, requestParams?: ParamsType ): Promise<BackendResponse> { return this.requestWithTaskHandling({ method: 'DELETE', path, requestParams, }); } async requestVisitorSession( sessionId: string, token: string ): Promise<VisitorSession> { return this.request({ method: 'PUT', path: `sessions/${sessionId}`, authorizationProvider: { authorize(request) { return request(`id_token ${token}`); }, }, }) as Promise<VisitorSession>; } // For test purpose only. currentPublicAuthorizationState(): string { if (this.config?.authorizationProvider) { if (this.config?.authorizationProvider.currentState) { return `[API] ${ this.config?.authorizationProvider.currentState() ?? 'null' }`; } return '[API]: authorization provider without currentState()'; } return '[API]: no authorization provider'; } private async requestWithTaskHandling({ method, path, requestParams, }: { method: string; path: string; requestParams?: ParamsType; }): Promise<BackendResponse> { const result = await this.request({ method, path, requestParams }); return isTaskResponse(result) ? this.handleTask(result.task) : result; } private async request({ method, path, requestParams, authorizationProvider, }: { method: string; path: string; requestParams?: ParamsType; authorizationProvider?: AuthorizationProvider | null; }): Promise<BackendResponse> { if (requestsAreDisabled) { // When connected to a UI, all communications of an SDK app with the backend // must go through the UI adapter. throw new InternalError('Unexpected CMS backend access.'); } const config = await this.configDeferred.promise; const tenant = await fetchConfiguredTenant(); const url = `${config.apiBaseUrl}/tenants/${tenant}/perform`; try { return await withLoginHandler( loginRedirectHandler, () => fetchJson(url, { method: method === 'POST' ? 'POST' : 'PUT', headers: getHeaders(config), data: { path, verb: method, params: requestParams, ...(config.analyticsProvider && config.analyticsProvider()), }, authProvider: getAuthorizationProviderForRequest( authorizationProvider, config ), credentials: 'omit', }) as Promise<Response> ); } catch (error) { throw error instanceof ClientError && error.code === 'precondition_not_met.workspace_not_found' ? new MissingWorkspaceError() : error; } } private async handleTask(task: TaskData): Promise<BackendResponse> { switch (task.status) { case 'success': return task.result; case 'error': throw new ClientError(task.message, task.code, {}); case 'exception': throw new RequestFailedError(task.message); case 'open': { await wait(2); const result = await this.get(`tasks/${task.id}`); return this.handleTask(result as TaskData); } default: throw new RequestFailedError('Invalid task response (unknown status)'); } } } function getHeaders(config: CmsRestApiConfig) { let headers: Record<string, string> = { 'Scrivito-Client': getClientVersion(), }; const priorityWithFallback = config.priority || fallbackPriority; if (priorityWithFallback === 'background') { headers = { ...headers, 'Scrivito-Priority': priorityWithFallback, }; } if (config.accessAsEditor) { headers = { ...headers, 'Scrivito-Access-As': 'editor', }; } return headers; } function getAuthorizationProviderForRequest( authorizationProvider: AuthorizationProvider | null | undefined, config: CmsRestApiConfig ) { if (authorizationProvider === undefined) { return config.authorizationProvider; } if (authorizationProvider === null) return undefined; return authorizationProvider; } function isTaskResponse(result: unknown): result is TaskResponse { return ( !!result && !!(result as TaskResponse).task && Object.keys(result as TaskResponse).length === 1 ); } export let cmsRestApi = new CmsRestApi(); // For test purpose only. export function resetCmsRestApi(): void { cmsRestApi = new CmsRestApi(); requestsAreDisabled = undefined; } onReset(resetCmsRestApi);