CommunitySolidServer/src/init/setup/SetupHttpHandler.ts
2022-09-26 11:57:42 +02:00

117 lines
4.4 KiB
TypeScript

import type { Operation } from '../../http/Operation';
import { OkResponseDescription } from '../../http/output/response/OkResponseDescription';
import type { ResponseDescription } from '../../http/output/response/ResponseDescription';
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
import type { InteractionHandler } from '../../identity/interaction/InteractionHandler';
import { getLoggerFor } from '../../logging/LogUtil';
import type { OperationHttpHandlerInput } from '../../server/OperationHttpHandler';
import { OperationHttpHandler } from '../../server/OperationHttpHandler';
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
import { APPLICATION_JSON, TEXT_HTML } from '../../util/ContentTypes';
import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
import type { TemplateEngine } from '../../util/templates/TemplateEngine';
export interface SetupHttpHandlerArgs {
/**
* Used for converting the input data.
*/
converter: RepresentationConverter;
/**
* Handles the requests.
*/
handler: InteractionHandler;
/**
* Key that is used to store the boolean in the storage indicating setup is finished.
*/
storageKey: string;
/**
* Used to store setup status.
*/
storage: KeyValueStorage<string, boolean>;
/**
* Renders the main view.
*/
templateEngine: TemplateEngine;
/**
* Determines if pods can be created in the root of the server.
* Relevant to make sure there are no nested storages if registration is enabled for example.
* Defaults to `true`.
*/
allowRootPod?: boolean;
}
/**
* Handles the initial setup of a server.
* Will capture all requests until setup is finished,
* this to prevent accidentally running unsafe servers.
*
* GET requests will return the view template which should contain the setup information for the user.
* POST requests will be sent to the InteractionHandler.
* After successfully completing a POST request this handler will disable itself and become unreachable.
* All other methods will be rejected.
*/
export class SetupHttpHandler extends OperationHttpHandler {
protected readonly logger = getLoggerFor(this);
private readonly handler: InteractionHandler;
private readonly converter: RepresentationConverter;
private readonly storageKey: string;
private readonly storage: KeyValueStorage<string, boolean>;
private readonly templateEngine: TemplateEngine;
private readonly allowRootPod: boolean;
public constructor(args: SetupHttpHandlerArgs) {
super();
this.handler = args.handler;
this.converter = args.converter;
this.storageKey = args.storageKey;
this.storage = args.storage;
this.templateEngine = args.templateEngine;
this.allowRootPod = args.allowRootPod ?? true;
}
public async handle({ operation }: OperationHttpHandlerInput): Promise<ResponseDescription> {
switch (operation.method) {
case 'GET': return this.handleGet(operation);
case 'POST': return this.handlePost(operation);
default: throw new MethodNotAllowedHttpError([ operation.method ]);
}
}
/**
* Returns the HTML representation of the setup page.
*/
private async handleGet(operation: Operation): Promise<ResponseDescription> {
const result = await this.templateEngine.handleSafe({ contents: { allowRootPod: this.allowRootPod }});
const representation = new BasicRepresentation(result, operation.target, TEXT_HTML);
return new OkResponseDescription(representation.metadata, representation.data);
}
/**
* Converts the input data to JSON and calls the setup handler.
* On success `true` will be written to the storage key.
*/
private async handlePost(operation: Operation): Promise<ResponseDescription> {
// Convert input data to JSON
// Allows us to still support form data
if (operation.body.metadata.contentType) {
const args = {
representation: operation.body,
preferences: { type: { [APPLICATION_JSON]: 1 }},
identifier: operation.target,
};
operation = {
...operation,
body: await this.converter.handleSafe(args),
};
}
const representation = await this.handler.handleSafe({ operation });
await this.storage.set(this.storageKey, true);
return new OkResponseDescription(representation.metadata, representation.data);
}
}