feat: Remove setup

This commit is contained in:
Joachim Van Herwegen 2023-07-17 10:00:48 +02:00
parent ea83ea59a1
commit 5eff035cb3
51 changed files with 5 additions and 930 deletions

View File

@ -19,16 +19,6 @@ This is the entry point to the main server setup.
* *default*: The main application. This should only be changed/replaced
if you want to start from a different kind of class.
## Setup
Handles the setup page the first time the server is started.
* *disabled*: Disables the setup page. Root container access will be impossible unless handled by the Init config above.
Registration and pod creation is still possible if that feature is enabled.
* *optional*: Setup is available at `/setup` but the server can already be used.
Everyone can access the setup page so make sure to complete that as soon as possible.
* *required*: All requests will be redirected to the setup page until setup is completed.
## Variables
Handles parsing CLI parameters and assigning values to Components.js variables.

View File

@ -1,10 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Completely disables the setup page.",
"@id": "urn:solid-server:default:SetupHandler",
"@type": "UnsupportedAsyncHandler"
}
]
}

View File

@ -1,20 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Redirects all request to the setup.",
"@id": "urn:solid-server:default:SetupRedirectHandler",
"@type": "RedirectingHttpHandler",
"redirects": [
{
"RedirectingHttpHandler:_redirects_key": ".*",
"RedirectingHttpHandler:_redirects_value": "/setup"
}
],
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"statusCode": 302
}
]
}

View File

@ -1,70 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Handles everything related to the first-time server setup.",
"@id": "urn:solid-server:default:SetupParsingHandler",
"@type": "ParsingHttpHandler",
"args_requestParser": { "@id": "urn:solid-server:default:RequestParser" },
"args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"args_operationHandler": {
"@id": "urn:solid-server:default:SetupHttpHandler",
"@type": "SetupHttpHandler",
"args_handler": {
"@type": "SetupHandler",
"args_initializer": { "@id": "urn:solid-server:default:SetupInitializer" },
"args_registrationManager": { "@id": "urn:solid-server:default:SetupRegistrationManager" }
},
"args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
"args_storageKey": "setupCompleted-2.0",
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" },
"args_templateEngine": {
"comment": "Renders the specific page and embeds it into the main HTML body.",
"@type": "ChainedTemplateEngine",
"renderedName": "htmlBody",
"engines": [
{
"comment": "Renders the main setup template.",
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/setup/index.html.ejs"
},
{
"comment": "Will embed the result of the first engine into the main HTML template.",
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/main.html.ejs"
}
]
}
}
},
{
"comment": "Separate manager from the RegistrationHandler in case registration is disabled.",
"@id": "urn:solid-server:default:SetupRegistrationManager",
"@type": "RegistrationManager",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_webIdSuffix": "/profile/card#me",
"args_identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" },
"args_ownershipValidator": { "@id": "urn:solid-server:auth:password:OwnershipValidator" },
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
"args_podManager": { "@id": "urn:solid-server:default:PodManager" }
},
{
"comment": "Separate initializer as we only want a simple one that sets the root .acl.",
"@id": "urn:solid-server:default:SetupInitializer",
"@type": "ContainerInitializer",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_path": "/",
"args_store": { "@id": "urn:solid-server:default:ResourceStore" },
"args_generator": {
"@type": "StaticFolderGenerator",
"templateFolder": "@css:templates/root/empty",
"resourcesGenerator": { "@id": "urn:solid-server:default:TemplatedResourcesGenerator" }
},
"args_storageKey": "rootInitialized",
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
}
]
}

View File

@ -1,23 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/setup/handlers/setup.json"
],
"@graph": [
{
"comment": "Combines both the redirect and the setup.",
"@id": "urn:solid-server:default:SetupHandler",
"@type": "ConditionalHandler",
"storageKey": "setupCompleted-2.0",
"storageValue": true,
"storage": { "@id": "urn:solid-server:default:SetupStorage" },
"source": {
"@type": "RouterHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"args_allowedPathNames": [ "/setup" ],
"args_handler": { "@id": "urn:solid-server:default:SetupParsingHandler" }
}
}
]
}

View File

@ -1,30 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/setup/handlers/redirect.json",
"css:config/app/setup/handlers/setup.json"
],
"@graph": [
{
"comment": "Combines both the redirect and the setup.",
"@id": "urn:solid-server:default:SetupHandler",
"@type": "ConditionalHandler",
"storageKey": "setupCompleted-2.0",
"storageValue": true,
"storage": { "@id": "urn:solid-server:default:SetupStorage" },
"source": {
"@type": "WaterfallHandler",
"handlers": [
{ "@id": "urn:solid-server:default:SetupRedirectHandler" },
{
"@type": "RouterHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"args_allowedPathNames": [ "/setup" ],
"args_handler": { "@id": "urn:solid-server:default:SetupParsingHandler" }
}
]
}
}
]
}

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-prefilled-root.json",
"css:config/app/setup/optional.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
@ -37,7 +36,7 @@
{
"comment": [
"A Solid server that stores its resources on disk and uses WAC for authorization.",
"No setup is required and the root container is initialized to allow full access for everyone so make sure to change this."
"No registration and the root container is initialized to allow full access for everyone so make sure to change this."
]
}
]

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -16,7 +16,6 @@
"@type": "WaterfallHandler",
"handlers": [
{ "@id": "urn:solid-server:default:StaticAssetHandler" },
{ "@id": "urn:solid-server:default:SetupHandler" },
{ "@id": "urn:solid-server:default:OidcHandler" },
{ "@id": "urn:solid-server:default:NotificationHttpHandler" },
{ "@id": "urn:solid-server:default:StorageDescriptionHandler" },

View File

@ -15,7 +15,6 @@
"@type": "WaterfallHandler",
"handlers": [
{ "@id": "urn:solid-server:default:StaticAssetHandler" },
{ "@id": "urn:solid-server:default:SetupHandler" },
{ "@id": "urn:solid-server:default:StorageDescriptionHandler" },
{ "@id": "urn:solid-server:default:LdpHandler" }
]

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -36,12 +36,6 @@
"HtmlViewHandler:_templates_value": { "@id": "urn:solid-server:auth:password:RegistrationRoute" }
}
]
},
{
"comment": "Root access is disabled when registration is enabled to prevent nested storage containers.",
"@id": "urn:solid-server:default:SetupHttpHandler",
"@type": "SetupHttpHandler",
"allowRootPod": false
}
]
}

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/optional.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/disabled.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
@ -38,7 +37,7 @@
"comment": [
"A single-pod server that stores its resources in a SPARQL endpoint and uses WAC for authorization.",
"This server only supports RDF data. For this reason it can not use its resource store for internal key/value storage.",
"No setup is required and the root container is initialized to allow full access for everyone so make sure to change this."
"No registration and the root container is initialized to allow full access for everyone so make sure to change this."
]
}
]

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",

View File

@ -19,7 +19,6 @@ flowchart LR
subgraph WaterfallHandlerArgs[" "]
direction TB
StaticAssetHandler("<strong>StaticAssetHandler</strong><br>StaticAssetHandler")
SetupHandler("<strong>SetupHandler</strong><br><i>HttpHandler</i>")
OidcHandler("<strong>OidcHandler</strong><br><i>HttpHandler</i>")
NotificationHttpHandler("<strong>NotificationHttpHandler</strong><br><i>HttpHandler</i>")
StorageDescriptionHandler("<strong>StorageDescriptionHandler</strong><br><i>HttpHandler</i>")
@ -28,8 +27,7 @@ flowchart LR
LdpHandler("<strong>LdpHandler</strong><br><i>HttpHandler</i>")
end
StaticAssetHandler --> SetupHandler
SetupHandler --> OidcHandler
StaticAssetHandler --> OidcHandler
OidcHandler --> NotificationHttpHandler
NotificationHttpHandler --> StorageDescriptionHandler
StorageDescriptionHandler --> AuthResourceHttpHandler
@ -52,17 +50,6 @@ An example of this is the favicon, where the `/favicon.ico` URL
is directed to the favicon file at `/templates/images/favicon.ico`.
It can also map entire folders to a specific path, such as `/.well-known/css/styles/` which contains all stylesheets.
## SetupHandler
The `urn:solid-server:default:SetupHandler` is responsible
for redirecting all requests to `/setup` until setup is finished,
thereby ensuring that setup needs to be finished before anything else can be done on the server,
and handling the actual setup request that is sent to `/setup`.
Once setup is finished, this handler will reject all requests and thus no longer be relevant.
If the server is configured to not have setup enabled,
the corresponding identifier will point to a handler that always rejects all requests.
## OidcHandler
The `urn:solid-server:default:OidcHandler` handles all requests related

View File

@ -134,7 +134,7 @@ To register a user, you can do a POST request with a JSON body containing the co
Two fields here that are not covered on the HTML page above are `rootPod` and `template`.
`rootPod` tells the server to put the pod in the root of the server instead of a location based on the `podName`.
By default the server will reject requests where this is `true`, except during setup.
By default the server will reject requests where this is `true`.
`template` is only used by servers running the `config/dynamic.json` configuration,
which is a very custom setup where every pod can have a different Components.js configuration,
so this value can usually be ignored.
@ -186,5 +186,3 @@ so they can be recreated when the server restarts.
### registration
This setting allows you to enable/disable registration on the server.
Disabling registration here does not disable registration during setup,
meaning you can still use this server as an IDP with the account created there.

View File

@ -56,7 +56,7 @@
"postrelease": "ts-node ./scripts/finalizeRelease.ts",
"start": "node ./bin/server.js",
"start:file": "node ./bin/server.js -c config/file.json -f ./data",
"start:file-no-setup": "node ./bin/server.js -c config/file-no-setup.json -f ./data",
"start:file-root": "node ./bin/server.js -c config/file-root.json -f ./data",
"test": "npm run test:ts && npm run jest",
"test:deploy": "test/deploy/validate-configs.sh",
"test:ts": "tsc -p test --noEmit",

View File

@ -209,10 +209,6 @@ export * from './init/final/Finalizable';
export * from './init/final/FinalizableHandler';
export * from './init/final/Finalizer';
// Init/Setup
export * from './init/setup/SetupHandler';
export * from './init/setup/SetupHttpHandler';
// Init/Cli
export * from './init/cli/CliExtractor';
export * from './init/cli/YargsCliExtractor';

View File

@ -1,83 +0,0 @@
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
import type { Representation } from '../../http/representation/Representation';
import { BaseInteractionHandler } from '../../identity/interaction/BaseInteractionHandler';
import type { RegistrationManager } from '../../identity/interaction/email-password/util/RegistrationManager';
import type { InteractionHandlerInput } from '../../identity/interaction/InteractionHandler';
import { getLoggerFor } from '../../logging/LogUtil';
import { APPLICATION_JSON } from '../../util/ContentTypes';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { readJsonStream } from '../../util/StreamUtil';
import type { Initializer } from '../Initializer';
export interface SetupHandlerArgs {
/**
* Used for registering a pod during setup.
*/
registrationManager?: RegistrationManager;
/**
* Initializer to call in case no registration procedure needs to happen.
* This Initializer should make sure the necessary resources are there so the server can work correctly.
*/
initializer?: Initializer;
}
/**
* On POST requests, runs an initializer and/or performs a registration step, both optional.
*/
export class SetupHandler extends BaseInteractionHandler {
protected readonly logger = getLoggerFor(this);
private readonly registrationManager?: RegistrationManager;
private readonly initializer?: Initializer;
public constructor(args: SetupHandlerArgs) {
super({});
this.registrationManager = args.registrationManager;
this.initializer = args.initializer;
}
protected async handlePost({ operation }: InteractionHandlerInput): Promise<Representation> {
const json = operation.body.isEmpty ? {} : await readJsonStream(operation.body.data);
const output: Record<string, any> = { initialize: false, registration: false };
if (json.registration) {
Object.assign(output, await this.register(json));
output.registration = true;
} else if (json.initialize) {
// We only want to initialize if no registration happened
await this.initialize();
output.initialize = true;
}
this.logger.debug(`Output: ${JSON.stringify(output)}`);
return new BasicRepresentation(JSON.stringify(output), APPLICATION_JSON);
}
/**
* Call the initializer.
* Errors if no initializer was defined.
*/
private async initialize(): Promise<void> {
if (!this.initializer) {
throw new NotImplementedHttpError('This server is not configured with a setup initializer.');
}
await this.initializer.handleSafe();
}
/**
* Register a user based on the given input.
* Errors if no registration manager is defined.
*/
private async register(json: NodeJS.Dict<any>): Promise<Record<string, any>> {
if (!this.registrationManager) {
throw new NotImplementedHttpError('This server is not configured to support registration during setup.');
}
// Validate the input JSON
const validated = this.registrationManager.validateInput(json, true);
this.logger.debug(`Validated input: ${JSON.stringify(validated)}`);
// Register and/or create a pod as requested. Potentially does nothing if all booleans are false.
return this.registrationManager.register(validated, true);
}
}

View File

@ -1,116 +0,0 @@
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);
}
}

View File

@ -1,45 +0,0 @@
<div id="input-partial">
<%- include('./input-partial.html.ejs') %>
</div>
<div id="response-partial">
<h1 id="public">Server setup complete</h1>
<p>
Congratulations!
Your Solid server is now ready to use.
<br>
You can now visit its <a href="./">homepage</a>.
</p>
<div id="response-initialize">
<h2>Root Pod</h2>
<p>
<strong>Warning: the root Pod is publicly accessible.</strong>
<br>
Prevent public write and control access to the root
by modifying its <a href=".acl">ACL document</a>.
</p>
</div>
<div id="response-registration">
<%- include('../identity/email-password/register-response-partial.html.ejs', { idpIndex: '' }) %>
</div>
</div>
<script>
function updateResponse(json) {
// Swap visibility
setVisibility('input-partial', false);
setVisibility('response-partial', true);
setVisibility('response-initialize', json.initialize);
setVisibility('response-registration', json.registration);
if (json.registration) {
updateResponseFields(json);
}
}
setVisibility('response-partial', false);
addPostListener('mainForm', 'error', '', updateResponse);
</script>

View File

@ -1,79 +0,0 @@
<h1>Set up your Solid server</h1>
<p>
Your Solid server needs a <strong>one-time setup</strong>
so it acts exactly the way you want.
</p>
<form method="post" id="mainForm">
<p class="error" id="error"></p>
<fieldset>
<legend>Accounts on this server</legend>
<ol>
<li class="checkbox">
<label>
<input type="checkbox" <% if (!allowRootPod) { %> checked <% } %> disabled>
Enable account registration.
</label>
<p>
This can only be changed in the configuration.
See the <a href="https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/config/README.md">general configuration documentation</a>
and the <a href="https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/config/identity/README.md">identity specific options</a> to find out how.
</p>
</li>
<li class="checkbox">
<label>
<input type="checkbox" id="registration" name="registration">
Sign me up for an account.
</label>
<p>
Any existing root Pod will be disabled.
</p>
</li>
<li class="checkbox" id="initializeForm">
<label>
<input type="checkbox" id="initialize" name="initialize" <% if (!allowRootPod) { %> disabled <% } %>>
Expose a public root Pod.
</label>
<p>
By default, the public has read and write access to the root Pod.
<br>
You typically only want to choose this
for rapid testing and development.
<br>
This requires registration to be disabled.
</p>
</li>
</ol>
</fieldset>
<fieldset id="registrationForm">
<legend>Sign up</legend>
<%-
include('../identity/email-password/register-partial.html.ejs', {
allowRootPod: allowRootPod,
})
%>
</fieldset>
<p class="actions"><button type="submit">Complete setup</button></p>
</form>
<!-- Show or hide the account creation form when needed -->
<script>
[
'registration', 'registrationForm', 'initializeForm',
].forEach(registerElement);
Object.assign(visibilityConditions, {
registrationForm: () => elements.registration.checked,
});
// Assigning elements to `visibilityConditions` causes the `disabled` to be removed.
// We don't want this for the initialize form.
const registration = document.getElementById('registration');
registration.addEventListener('change', () => {
const initializeForm = document.getElementById('initializeForm');
initializeForm.classList[elements.registration.checked ? 'add' : 'remove']('hidden');
});
</script>

View File

@ -1,128 +0,0 @@
import fetch from 'cross-fetch';
import type { App } from '../../src/init/App';
import { joinUrl } from '../../src/util/PathUtil';
import { getPort } from '../util/Util';
import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
const port = getPort('SetupMemory');
const baseUrl = `http://localhost:${port}/`;
// Some tests with real Requests/Responses until the mocking library has been removed from the tests
describe('A Solid server with setup', (): void => {
const email = 'test@test.email';
const password = 'password!';
const podName = 'test';
const setupUrl = joinUrl(baseUrl, '/setup');
let app: App;
// `beforeEach` since the server needs to restart to reset setup
beforeEach(async(): Promise<void> => {
const instances = await instantiateFromConfig(
'urn:solid-server:test:Instances',
getTestConfigPath('setup-memory.json'),
getDefaultVariables(port, baseUrl),
) as Record<string, any>;
({ app } = instances);
await app.start();
});
afterEach(async(): Promise<void> => {
await app.stop();
});
it('catches all requests.', async(): Promise<void> => {
let res = await fetch(baseUrl, { method: 'GET', headers: { accept: 'text/html' }});
expect(res.status).toBe(200);
expect(res.url).toBe(setupUrl);
await expect(res.text()).resolves.toContain('Set up your Solid server');
res = await fetch(joinUrl(baseUrl, '/random/path/'), { method: 'GET', headers: { accept: 'text/html' }});
expect(res.status).toBe(200);
expect(res.url).toBe(setupUrl);
await expect(res.text()).resolves.toContain('Set up your Solid server');
res = await fetch(joinUrl(baseUrl, '/random/path/'), { method: 'PUT' });
expect(res.status).toBe(405);
expect(res.url).toBe(setupUrl);
await expect(res.json()).resolves.toEqual(expect.objectContaining({ name: 'MethodNotAllowedHttpError' }));
});
it('can create a server that disables root but allows registration.', async(): Promise<void> => {
let res = await fetch(setupUrl, { method: 'POST' });
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({ initialize: false, registration: false });
// Root access disabled
res = await fetch(baseUrl);
expect(res.status).toBe(401);
// Registration still possible
const registerParams = { email, podName, password, confirmPassword: password, createWebId: true };
res = await fetch(joinUrl(baseUrl, 'idp/register/'), {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(registerParams),
});
expect(res.status).toBe(200);
res = await fetch(joinUrl(baseUrl, podName, '/profile/card'));
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain('foaf:PersonalProfileDocument');
});
it('can create a server with a public root.', async(): Promise<void> => {
let res = await fetch(setupUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ initialize: true }),
});
expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({ initialize: true, registration: false });
// Root access enabled
res = await fetch(baseUrl);
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain('<> a <http://www.w3.org/ns/pim/space#Storage>');
// Root pod registration is never allowed
const registerParams = { email, podName, password, confirmPassword: password, createWebId: true, rootPod: true };
res = await fetch(joinUrl(baseUrl, 'idp/register/'), {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(registerParams),
});
expect(res.status).toBe(500);
});
it('can create a server with a root pod.', async(): Promise<void> => {
const registerParams = { email, podName, password, confirmPassword: password, createWebId: true, rootPod: true };
let res = await fetch(setupUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ registration: true, initialize: true, ...registerParams }),
});
expect(res.status).toBe(200);
const json = await res.json();
expect(json).toEqual(expect.objectContaining({
registration: true,
initialize: false,
oidcIssuer: baseUrl,
webId: `${baseUrl}profile/card#me`,
email,
podBaseUrl: baseUrl,
}));
// Root profile created
res = await fetch(joinUrl(baseUrl, '/profile/card'));
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain('foaf:PersonalProfileDocument');
// Pod root is not accessible even though initialize was set to true
res = await fetch(joinUrl(baseUrl, 'resource'), {
method: 'PUT',
headers: { 'content-type': 'text/plain' },
body: 'random data',
});
expect(res.status).toBe(401);
});
});

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/simple.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/disabled.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/simple.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/disabled.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/legacy-websockets.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/simple.json",
"css:config/http/middleware/default.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/disabled.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/disabled.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/simple.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/simple.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/disabled.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/disabled.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/simple.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",

View File

@ -1,47 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/setup/required.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",
"css:config/http/server-factory/http.json",
"css:config/http/static/default.json",
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
"css:config/ldp/metadata-parser/default.json",
"css:config/ldp/metadata-writer/default.json",
"css:config/ldp/modes/default.json",
"css:config/storage/backend/memory.json",
"css:config/storage/key-value/resource-store.json",
"css:config/storage/middleware/default.json",
"css:config/util/auxiliary/acl.json",
"css:config/util/identifiers/suffix.json",
"css:config/util/index/default.json",
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/memory.json",
"css:config/util/variables/default.json"
],
"@graph": [
{
"@id": "urn:solid-server:test:Instances",
"@type": "RecordObject",
"record": [
{
"RecordObject:_record_key": "app",
"RecordObject:_record_value": { "@id": "urn:solid-server:default:App" }
}
]
}
]
}

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/webhooks.json",

View File

@ -3,7 +3,6 @@
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-root.json",
"css:config/app/setup/disabled.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/websockets.json",

View File

@ -1,88 +0,0 @@
import type { Operation } from '../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { RegistrationResponse,
RegistrationManager } from '../../../../src/identity/interaction/email-password/util/RegistrationManager';
import type { Initializer } from '../../../../src/init/Initializer';
import { SetupHandler } from '../../../../src/init/setup/SetupHandler';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { readJsonStream } from '../../../../src/util/StreamUtil';
describe('A SetupHandler', (): void => {
let operation: Operation;
let details: RegistrationResponse;
let registrationManager: jest.Mocked<RegistrationManager>;
let initializer: jest.Mocked<Initializer>;
let handler: SetupHandler;
beforeEach(async(): Promise<void> => {
operation = {
method: 'POST',
target: { path: 'http://example.com/setup' },
preferences: {},
body: new BasicRepresentation(),
};
initializer = {
handleSafe: jest.fn(),
} as any;
details = {
email: 'alice@test.email',
createWebId: true,
register: true,
createPod: true,
};
registrationManager = {
validateInput: jest.fn((input): any => input),
register: jest.fn().mockResolvedValue(details),
} as any;
handler = new SetupHandler({ registrationManager, initializer });
});
it('error if no Initializer is defined and initialization is requested.', async(): Promise<void> => {
handler = new SetupHandler({});
operation.body = new BasicRepresentation(JSON.stringify({ initialize: true }), 'application/json');
await expect(handler.handle({ operation })).rejects.toThrow(NotImplementedHttpError);
});
it('error if no RegistrationManager is defined and registration is requested.', async(): Promise<void> => {
handler = new SetupHandler({});
operation.body = new BasicRepresentation(JSON.stringify({ registration: true }), 'application/json');
await expect(handler.handle({ operation })).rejects.toThrow(NotImplementedHttpError);
});
it('calls the Initializer when requested.', async(): Promise<void> => {
operation.body = new BasicRepresentation(JSON.stringify({ initialize: true }), 'application/json');
const result = await handler.handle({ operation });
await expect(readJsonStream(result.data)).resolves.toEqual({ initialize: true, registration: false });
expect(result.metadata.contentType).toBe('application/json');
expect(initializer.handleSafe).toHaveBeenCalledTimes(1);
expect(registrationManager.validateInput).toHaveBeenCalledTimes(0);
expect(registrationManager.register).toHaveBeenCalledTimes(0);
});
it('calls the RegistrationManager when requested.', async(): Promise<void> => {
const body = { registration: true, email: 'test@example.com' };
operation.body = new BasicRepresentation(JSON.stringify(body), 'application/json');
const result = await handler.handle({ operation });
await expect(readJsonStream(result.data)).resolves.toEqual({ initialize: false, registration: true, ...details });
expect(result.metadata.contentType).toBe('application/json');
expect(initializer.handleSafe).toHaveBeenCalledTimes(0);
expect(registrationManager.validateInput).toHaveBeenCalledTimes(1);
expect(registrationManager.register).toHaveBeenCalledTimes(1);
expect(registrationManager.validateInput).toHaveBeenLastCalledWith(body, true);
expect(registrationManager.register).toHaveBeenLastCalledWith(body, true);
});
it('defaults to an empty JSON body if no data is provided.', async(): Promise<void> => {
operation.body = new BasicRepresentation();
const result = await handler.handle({ operation });
await expect(readJsonStream(result.data)).resolves.toEqual({ initialize: false, registration: false });
expect(result.metadata.contentType).toBe('application/json');
expect(initializer.handleSafe).toHaveBeenCalledTimes(0);
expect(registrationManager.validateInput).toHaveBeenCalledTimes(0);
expect(registrationManager.register).toHaveBeenCalledTimes(0);
});
});

View File

@ -1,119 +0,0 @@
import type { Operation } from '../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../src/http/representation/Representation';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import type { InteractionHandler } from '../../../../src/identity/interaction/InteractionHandler';
import { SetupHttpHandler } from '../../../../src/init/setup/SetupHttpHandler';
import type { HttpRequest } from '../../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../../src/server/HttpResponse';
import { getBestPreference } from '../../../../src/storage/conversion/ConversionUtil';
import type { RepresentationConverterArgs,
RepresentationConverter } from '../../../../src/storage/conversion/RepresentationConverter';
import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage';
import { APPLICATION_JSON, APPLICATION_X_WWW_FORM_URLENCODED } from '../../../../src/util/ContentTypes';
import { MethodNotAllowedHttpError } from '../../../../src/util/errors/MethodNotAllowedHttpError';
import { readableToString } from '../../../../src/util/StreamUtil';
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
describe('A SetupHttpHandler', (): void => {
const request: HttpRequest = {} as any;
const response: HttpResponse = {} as any;
let operation: Operation;
const storageKey = 'completed';
let representation: Representation;
let interactionHandler: jest.Mocked<InteractionHandler>;
let templateEngine: jest.Mocked<TemplateEngine>;
let converter: jest.Mocked<RepresentationConverter>;
let storage: jest.Mocked<KeyValueStorage<string, any>>;
let handler: SetupHttpHandler;
beforeEach(async(): Promise<void> => {
operation = {
method: 'GET',
target: { path: 'http://example.com/setup' },
preferences: {},
body: new BasicRepresentation(),
};
templateEngine = {
handleSafe: jest.fn().mockReturnValue(Promise.resolve('<html>')),
} as any;
converter = {
handleSafe: jest.fn((input: RepresentationConverterArgs): Representation => {
// Just find the best match;
const type = getBestPreference(input.preferences.type!, { '*/*': 1 })!;
const metadata = new RepresentationMetadata(input.representation.metadata, { [CONTENT_TYPE]: type.value });
return new BasicRepresentation(input.representation.data, metadata);
}),
} as any;
representation = new BasicRepresentation();
interactionHandler = {
handleSafe: jest.fn().mockResolvedValue(representation),
} as any;
storage = new Map<string, any>() as any;
handler = new SetupHttpHandler({
converter,
storageKey,
storage,
handler: interactionHandler,
templateEngine,
});
});
it('only accepts GET and POST operations.', async(): Promise<void> => {
operation = {
method: 'DELETE',
target: { path: 'http://example.com/setup' },
preferences: {},
body: new BasicRepresentation(),
};
await expect(handler.handle({ operation, request, response })).rejects.toThrow(MethodNotAllowedHttpError);
});
it('calls the template engine for GET requests.', async(): Promise<void> => {
const result = await handler.handle({ operation, request, response });
expect(result.data).toBeDefined();
await expect(readableToString(result.data!)).resolves.toBe('<html>');
expect(result.metadata?.contentType).toBe('text/html');
// Setup is still enabled since this was a GET request
expect(storage.get(storageKey)).toBeUndefined();
});
it('returns the handler result as 200 response.', async(): Promise<void> => {
operation.method = 'POST';
const result = await handler.handle({ operation, request, response });
expect(result.statusCode).toBe(200);
expect(result.data).toBe(representation.data);
expect(result.metadata).toBe(representation.metadata);
expect(interactionHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(interactionHandler.handleSafe).toHaveBeenLastCalledWith({ operation });
// Handler is now disabled due to successful POST
expect(storage.get(storageKey)).toBe(true);
});
it('converts input bodies to JSON.', async(): Promise<void> => {
operation.method = 'POST';
operation.body.metadata.contentType = APPLICATION_X_WWW_FORM_URLENCODED;
const result = await handler.handle({ operation, request, response });
expect(result.statusCode).toBe(200);
expect(result.data).toBe(representation.data);
expect(result.metadata).toBe(representation.metadata);
expect(interactionHandler.handleSafe).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { body, ...partialOperation } = operation;
expect(interactionHandler.handleSafe).toHaveBeenLastCalledWith(
{ operation: expect.objectContaining(partialOperation) },
);
expect(interactionHandler.handleSafe.mock.calls[0][0].operation.body.metadata.contentType).toBe(APPLICATION_JSON);
// Handler is now disabled due to successful POST
expect(storage.get(storageKey)).toBe(true);
});
});