import type { Operation } from '../../../ldp/operations/Operation'; import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError'; import { createErrorMessage } from '../../../util/errors/ErrorUtil'; import { trimTrailingSlashes } from '../../../util/PathUtil'; import type { InteractionResponseResult, InteractionHandler, Interaction, } from '../email-password/handler/InteractionHandler'; import { IdpInteractionError } from '../util/IdpInteractionError'; import type { InteractionRoute, TemplatedInteractionResult } from './InteractionRoute'; /** * Default implementation of the InteractionRoute. * See function comments for specifics. */ export class BasicInteractionRoute implements InteractionRoute { public readonly route: RegExp; public readonly handler: InteractionHandler; public readonly viewTemplates: Record; public readonly prompt?: string; public readonly responseTemplates: Record; public readonly controls: Record; /** * @param route - Regex to match this route. * @param viewTemplates - Templates to render on GET requests. * Keys are content-types, values paths to a template. * @param handler - Handler to call on POST requests. * @param prompt - In case of requests to the IDP entry point, the session prompt will be compared to this. * @param responseTemplates - Templates to render as a response to POST requests when required. * Keys are content-types, values paths to a template. * @param controls - Controls to add to the response JSON. * The keys will be copied and the values will be converted to full URLs. */ public constructor(route: string, viewTemplates: Record, handler: InteractionHandler, prompt?: string, responseTemplates: Record = {}, controls: Record = {}) { this.route = new RegExp(route, 'u'); this.viewTemplates = viewTemplates; this.handler = handler; this.prompt = prompt; this.responseTemplates = responseTemplates; this.controls = controls; } /** * Returns the stored controls. */ public getControls(): Record { return this.controls; } /** * Checks support by comparing the prompt if the path targets the base URL, * and otherwise comparing with the stored route regular expression. */ public supportsPath(path: string, prompt?: string): boolean { // In case the request targets the IDP entry point the prompt determines where to go if (trimTrailingSlashes(path).length === 0 && prompt) { return this.prompt === prompt; } return this.route.test(path); } /** * GET requests return a default response result. * POST requests return the InteractionHandler result. * InteractionHandler errors will be converted into response results. * * All results will be appended with the matching template paths. * * Will error for other methods */ public async handleOperation(operation: Operation, oidcInteraction?: Interaction): Promise { switch (operation.method) { case 'GET': return { type: 'response', templateFiles: this.viewTemplates }; case 'POST': try { const result = await this.handler.handleSafe({ operation, oidcInteraction }); return { ...result, templateFiles: this.responseTemplates }; } catch (error: unknown) { // Render error in the view const errorMessage = createErrorMessage(error); const result: InteractionResponseResult = { type: 'response', details: { errorMessage }}; if (IdpInteractionError.isInstance(error)) { result.details!.prefilled = error.prefilled; } return { ...result, templateFiles: this.viewTemplates }; } default: throw new BadRequestHttpError(`Unsupported request: ${operation.method} ${operation.target.path}`); } } }