mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Move InteractionRoute behaviour to separate class
This commit is contained in:
@@ -14,59 +14,16 @@ import type { HttpResponse } from '../server/HttpResponse';
|
||||
import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
|
||||
import { APPLICATION_JSON } from '../util/ContentTypes';
|
||||
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
|
||||
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
||||
import { joinUrl, trimTrailingSlashes } from '../util/PathUtil';
|
||||
import { addTemplateMetadata } from '../util/ResourceUtil';
|
||||
import type { ProviderFactory } from './configuration/ProviderFactory';
|
||||
import type {
|
||||
Interaction,
|
||||
InteractionHandler,
|
||||
InteractionHandlerResult,
|
||||
InteractionResponseResult,
|
||||
} from './interaction/email-password/handler/InteractionHandler';
|
||||
import { IdpInteractionError } from './interaction/util/IdpInteractionError';
|
||||
import type { Interaction } from './interaction/email-password/handler/InteractionHandler';
|
||||
import type { InteractionRoute, TemplatedInteractionResult } from './interaction/routing/InteractionRoute';
|
||||
import type { InteractionCompleter } from './interaction/util/InteractionCompleter';
|
||||
|
||||
// Registration is not standardized within Solid yet, so we use a custom versioned API for now
|
||||
const API_VERSION = '0.2';
|
||||
|
||||
/**
|
||||
* All the information that is required to handle a request to a custom IDP path.
|
||||
*/
|
||||
export class InteractionRoute {
|
||||
public readonly route: RegExp;
|
||||
public readonly handler: InteractionHandler;
|
||||
public readonly viewTemplates: Record<string, string>;
|
||||
public readonly prompt?: string;
|
||||
public readonly responseTemplates: Record<string, string>;
|
||||
public readonly controls: Record<string, string>;
|
||||
|
||||
/**
|
||||
* @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<string, string>,
|
||||
handler: InteractionHandler,
|
||||
prompt?: string,
|
||||
responseTemplates: Record<string, string> = {},
|
||||
controls: Record<string, string> = {}) {
|
||||
this.route = new RegExp(route, 'u');
|
||||
this.viewTemplates = viewTemplates;
|
||||
this.handler = handler;
|
||||
this.prompt = prompt;
|
||||
this.responseTemplates = responseTemplates;
|
||||
this.controls = controls;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs {
|
||||
// Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73
|
||||
requestParser: RequestParser;
|
||||
@@ -147,6 +104,7 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
let oidcInteraction: Interaction | undefined;
|
||||
try {
|
||||
const provider = await this.providerFactory.getProvider();
|
||||
// This being defined means we're in an OIDC session
|
||||
oidcInteraction = await provider.interactionDetails(request, response);
|
||||
} catch {
|
||||
// Just a regular request
|
||||
@@ -175,8 +133,8 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
operation.body = await this.converter.handleSafe(args);
|
||||
}
|
||||
|
||||
const { result, templateFiles } = await this.resolveRoute(operation, route, oidcInteraction);
|
||||
return this.handleInteractionResult(operation, request, result, templateFiles, oidcInteraction);
|
||||
const result = await route.handleOperation(operation, oidcInteraction);
|
||||
return this.handleInteractionResult(operation, request, result, oidcInteraction);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,57 +147,19 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
}
|
||||
const pathName = operation.target.path.slice(this.baseUrl.length);
|
||||
|
||||
// In case the request targets the IDP entry point the prompt determines where to go
|
||||
const checkPrompt = oidcInteraction && trimTrailingSlashes(pathName).length === 0;
|
||||
|
||||
for (const route of this.interactionRoutes) {
|
||||
if (checkPrompt) {
|
||||
if (route.prompt === oidcInteraction!.prompt.name) {
|
||||
return route;
|
||||
}
|
||||
} else if (route.route.test(pathName)) {
|
||||
if (route.supportsPath(pathName, oidcInteraction?.prompt.name)) {
|
||||
return route;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the behaviour of an InteractionRoute.
|
||||
* Will error if the route does not support the given request.
|
||||
*
|
||||
* GET requests return a default response result,
|
||||
* POST requests to the specific InteractionHandler of the route.
|
||||
*/
|
||||
private async resolveRoute(operation: Operation, route: InteractionRoute, oidcInteraction?: Interaction):
|
||||
Promise<{ result: InteractionHandlerResult; templateFiles: Record<string, string> }> {
|
||||
if (operation.method === 'GET') {
|
||||
return { result: { type: 'response' }, templateFiles: route.viewTemplates };
|
||||
}
|
||||
|
||||
if (operation.method === 'POST') {
|
||||
try {
|
||||
const result = await route.handler.handleSafe({ operation, oidcInteraction });
|
||||
return { result, templateFiles: route.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: route.viewTemplates };
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestHttpError(`Unsupported request: ${operation.method} ${operation.target.path}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ResponseDescription based on the InteractionHandlerResult.
|
||||
* This will either be a redirect if type is "complete" or a data stream if the type is "response".
|
||||
*/
|
||||
private async handleInteractionResult(operation: Operation, request: HttpRequest, result: InteractionHandlerResult,
|
||||
templateFiles: Record<string, string>, oidcInteraction?: Interaction): Promise<ResponseDescription> {
|
||||
private async handleInteractionResult(operation: Operation, request: HttpRequest,
|
||||
result: TemplatedInteractionResult, oidcInteraction?: Interaction): Promise<ResponseDescription> {
|
||||
let responseDescription: ResponseDescription | undefined;
|
||||
|
||||
if (result.type === 'complete') {
|
||||
@@ -254,7 +174,7 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
responseDescription = new RedirectResponseDescription(location);
|
||||
} else {
|
||||
// Convert the response object to a data stream
|
||||
responseDescription = await this.handleResponseResult(result, templateFiles, operation, oidcInteraction);
|
||||
responseDescription = await this.handleResponseResult(result, operation, oidcInteraction);
|
||||
}
|
||||
|
||||
return responseDescription;
|
||||
@@ -264,8 +184,8 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
* Converts an InteractionResponseResult to a ResponseDescription by first converting to a Representation
|
||||
* and applying necessary conversions.
|
||||
*/
|
||||
private async handleResponseResult(result: InteractionResponseResult, templateFiles: Record<string, string>,
|
||||
operation: Operation, oidcInteraction?: Interaction): Promise<ResponseDescription> {
|
||||
private async handleResponseResult(result: TemplatedInteractionResult, operation: Operation,
|
||||
oidcInteraction?: Interaction): Promise<ResponseDescription> {
|
||||
// Convert the object to a valid JSON representation
|
||||
const json = {
|
||||
apiVersion: API_VERSION,
|
||||
@@ -276,7 +196,7 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
const representation = new BasicRepresentation(JSON.stringify(json), operation.target, APPLICATION_JSON);
|
||||
|
||||
// Template metadata is required for conversion
|
||||
for (const [ type, templateFile ] of Object.entries(templateFiles)) {
|
||||
for (const [ type, templateFile ] of Object.entries(result.templateFiles)) {
|
||||
addTemplateMetadata(representation.metadata, templateFile, type);
|
||||
}
|
||||
|
||||
@@ -291,8 +211,8 @@ export class IdentityProviderHttpHandler extends BaseHttpHandler {
|
||||
* Converts the controls object of a route to one with full URLs.
|
||||
*/
|
||||
private getRouteControls(route: InteractionRoute): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(route.controls).map(([ name, path ]): [ string, string ] => [ name, joinUrl(this.baseUrl, path) ]),
|
||||
);
|
||||
const entries = Object.entries(route.getControls())
|
||||
.map(([ name, path ]): [ string, string ] => [ name, joinUrl(this.baseUrl, path) ]);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
}
|
||||
|
||||
100
src/identity/interaction/routing/BasicInteractionRoute.ts
Normal file
100
src/identity/interaction/routing/BasicInteractionRoute.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
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<string, string>;
|
||||
public readonly prompt?: string;
|
||||
public readonly responseTemplates: Record<string, string>;
|
||||
public readonly controls: Record<string, string>;
|
||||
|
||||
/**
|
||||
* @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<string, string>,
|
||||
handler: InteractionHandler,
|
||||
prompt?: string,
|
||||
responseTemplates: Record<string, string> = {},
|
||||
controls: Record<string, string> = {}) {
|
||||
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<string, string> {
|
||||
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<TemplatedInteractionResult> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/identity/interaction/routing/InteractionRoute.ts
Normal file
33
src/identity/interaction/routing/InteractionRoute.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Operation } from '../../../ldp/operations/Operation';
|
||||
import type { Interaction, InteractionHandlerResult } from '../email-password/handler/InteractionHandler';
|
||||
|
||||
export type TemplatedInteractionResult = InteractionHandlerResult & {
|
||||
templateFiles: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the routing behaviour for IDP handlers.
|
||||
*/
|
||||
export interface InteractionRoute {
|
||||
/**
|
||||
* Returns the control fields that should be added to response objects.
|
||||
* Keys are control names, values are relative URL paths.
|
||||
*/
|
||||
getControls: () => Record<string, string>;
|
||||
|
||||
/**
|
||||
* If this route supports the given path.
|
||||
* @param path - Relative URL path.
|
||||
* @param prompt - Session prompt if there is one.
|
||||
*/
|
||||
supportsPath: (path: string, prompt?: string) => boolean;
|
||||
|
||||
/**
|
||||
* Handles the given operation.
|
||||
* @param operation - Operation to handle.
|
||||
* @param oidcInteraction - Interaction if there is one.
|
||||
*
|
||||
* @returns InteractionHandlerResult appended with relevant template files.
|
||||
*/
|
||||
handleOperation: (operation: Operation, oidcInteraction?: Interaction) => Promise<TemplatedInteractionResult>;
|
||||
}
|
||||
@@ -41,6 +41,10 @@ export * from './identity/interaction/email-password/storage/BaseAccountStore';
|
||||
// Identity/Interaction/Email-Password
|
||||
export * from './identity/interaction/email-password/EmailPasswordUtil';
|
||||
|
||||
// Identity/Interaction/Routing
|
||||
export * from './identity/interaction/routing/BasicInteractionRoute';
|
||||
export * from './identity/interaction/routing/InteractionRoute';
|
||||
|
||||
// Identity/Interaction/Util
|
||||
export * from './identity/interaction/util/BaseEmailSender';
|
||||
export * from './identity/interaction/util/EmailSender';
|
||||
|
||||
Reference in New Issue
Block a user