feat: Integrate setup behaviour

This adds options for enabling setup to the config folder.
All default configs with permanent storage (file/sparql)
are configured to require setup at server start.
Memory-based configs merely have it as an option.
This commit is contained in:
Joachim Van Herwegen 2021-09-15 16:56:18 +02:00
parent 4e1a2f5981
commit b592d449eb
47 changed files with 883 additions and 246 deletions

View File

@ -8,4 +8,15 @@ This is the entry point to the main server setup.
## Init
Contains a list of initializer that need to be run when starting the server.
* *default*: The default setup that makes sure the root container has the necessary resources.
* *default*: The default setup. The ParallelHandler can be used to add custom Initializers.
* *initialize-root*: Makes sure the root container has the necessary resources to function properly.
This is only relevant if setup is disabled but root container access is still required.
* *initialize-prefilled-root*: Similar to `initialize-root` but adds some introductory resources to the root container.
## 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.

View File

@ -1,8 +1,7 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/init/base/init.json",
"files-scs:config/app/init/initializers/root.json"
"files-scs:config/app/init/base/init.json"
],
"@graph": [
{
@ -10,7 +9,10 @@
"@id": "urn:solid-server:default:ParallelInitializer",
"@type": "ParallelHandler",
"handlers": [
{ "@id": "urn:solid-server:default:RootInitializer" }
{
"comment": "This handler is here because having this array empty gives Components.js errors.",
"@type": "StaticHandler"
}
]
}
]

View File

@ -0,0 +1,17 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/init/base/init.json",
"files-scs:config/app/init/initializers/prefilled-root.json"
],
"@graph": [
{
"comment": "These handlers are called whenever the server is started, and can be used to ensure that all necessary resources for booting are available.",
"@id": "urn:solid-server:default:ParallelInitializer",
"@type": "ParallelHandler",
"handlers": [
{ "@id": "urn:solid-server:default:RootInitializer" }
]
}
]
}

View File

@ -0,0 +1,17 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/init/base/init.json",
"files-scs:config/app/init/initializers/root.json"
],
"@graph": [
{
"comment": "These handlers are called whenever the server is started, and can be used to ensure that all necessary resources for booting are available.",
"@id": "urn:solid-server:default:ParallelInitializer",
"@type": "ParallelHandler",
"handlers": [
{ "@id": "urn:solid-server:default:RootInitializer" }
]
}
]
}

View File

@ -0,0 +1,18 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Makes sure the root container exists and contains the necessary resources.",
"@id": "urn:solid-server:default:RootInitializer",
"@type": "RootInitializer",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"store": { "@id": "urn:solid-server:default:ResourceStore" },
"generator": {
"@type": "TemplatedResourcesGenerator",
"templateFolder": "@css:templates/root/prefilled",
"factory": { "@type": "ExtensionBasedMapperFactory" },
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
}
}
]
}

View File

@ -9,7 +9,7 @@
"store": { "@id": "urn:solid-server:default:ResourceStore" },
"generator": {
"@type": "TemplatedResourcesGenerator",
"templateFolder": "@css:templates/root",
"templateFolder": "@css:templates/root/empty",
"factory": { "@type": "ExtensionBasedMapperFactory" },
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
}

View File

@ -0,0 +1,13 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/init/initializers/root.json"
],
"@graph": [
{
"comment": "Completely disables the setup page.",
"@id": "urn:solid-server:default:SetupHandler",
"@type": "UnsupportedAsyncHandler"
}
]
}

View File

@ -0,0 +1,14 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Redirects all request to the setup.",
"@id": "urn:solid-server:default:SetupRedirectHandler",
"@type": "RedirectAllHttpHandler",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_target": "/setup",
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }
},
]
}

View File

@ -0,0 +1,34 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/init/initializers/root.json"
],
"@graph": [
{
"comment": "Handles everything related to the first-time server setup.",
"@id": "urn:solid-server:default:SetupHttpHandler",
"@type": "SetupHttpHandler",
"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_initializer": { "@id": "urn:solid-server:default:RootInitializer" },
"args_registrationManager": { "@id": "urn:solid-server:default:SetupRegistrationManager" },
"args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
"args_storageKey": "setupCompleted-1.0",
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" },
"args_viewTemplate": "@css:templates/setup/index.html.ejs",
"args_responseTemplate": "@css:templates/setup/response.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" }
}
]
}

View File

@ -0,0 +1,24 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs: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-1.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_allowedMethods": [ "*" ],
"args_allowedPathNames": [ "/setup" ],
"args_handler": { "@id": "urn:solid-server:default:SetupHttpHandler" }
}
}
]
}

View File

@ -0,0 +1,31 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/setup/handlers/redirect.json",
"files-scs: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-1.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_allowedMethods": [ "*" ],
"args_allowedPathNames": [ "/setup" ],
"args_handler": { "@id": "urn:solid-server:default:SetupHttpHandler" }
}
]
}
}
]
}

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-prefilled-root.json",
"files-scs:config/app/setup/optional.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",

View File

@ -3,6 +3,7 @@
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/setup/required.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",

View File

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

38
config/file-no-setup.json Normal file
View File

@ -0,0 +1,38 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.json",
"files-scs:config/identity/email/default.json",
"files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json",
"files-scs:config/identity/pod/static.json",
"files-scs:config/identity/registration/enabled.json",
"files-scs:config/ldp/authentication/dpop-bearer.json",
"files-scs:config/ldp/authorization/webacl.json",
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json",
"files-scs:config/storage/backend/file.json",
"files-scs:config/storage/key-value/resource-store.json",
"files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json",
"files-scs:config/util/identifiers/suffix.json",
"files-scs:config/util/index/default.json",
"files-scs:config/util/logging/winston.json",
"files-scs:config/util/representation-conversion/default.json",
"files-scs:config/util/resource-locker/memory.json",
"files-scs:config/util/variables/default.json"
],
"@graph": [
{
"comment": "A single-pod server that stores its resources on disk."
}
]
}

View File

@ -3,6 +3,7 @@
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/setup/required.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",

View File

@ -1,5 +1,8 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/init/initializers/root.json"
],
"@graph": [
{
"comment": "These are all the handlers a request will go through until it is handled.",
@ -11,6 +14,7 @@
"@type": "WaterfallHandler",
"handlers": [
{ "@id": "urn:solid-server:default:StaticAssetHandler" },
{ "@id": "urn:solid-server:default:SetupHandler" },
{ "@id": "urn:solid-server:default:IdentityProviderHandler" },
{ "@id": "urn:solid-server:default:LdpHandler" }
]

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/optional.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",

View File

@ -0,0 +1,41 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",
"files-scs:config/http/static/default.json",
"files-scs:config/identity/email/default.json",
"files-scs:config/identity/handler/default.json",
"files-scs:config/identity/ownership/token.json",
"files-scs:config/identity/pod/static.json",
"files-scs:config/identity/registration/enabled.json",
"files-scs:config/ldp/authentication/dpop-bearer.json",
"files-scs:config/ldp/authorization/webacl.json",
"files-scs:config/ldp/handler/default.json",
"files-scs:config/ldp/metadata-parser/default.json",
"files-scs:config/ldp/metadata-writer/default.json",
"files-scs:config/ldp/permissions/acl.json",
"files-scs:config/storage/backend/sparql.json",
"files-scs:config/storage/key-value/memory.json",
"files-scs:config/storage/middleware/default.json",
"files-scs:config/util/auxiliary/acl.json",
"files-scs:config/util/identifiers/suffix.json",
"files-scs:config/util/index/default.json",
"files-scs:config/util/logging/winston.json",
"files-scs:config/util/representation-conversion/default.json",
"files-scs:config/util/resource-locker/memory.json",
"files-scs:config/util/variables/default.json"
],
"@graph": [
{
"comment": [
"A single-pod server that stores its resources in a SPARQL endpoint.",
"This server only supports RDF data. For this reason it can not use its resource store for internal key/value storage."
]
}
]
}

View File

@ -3,6 +3,7 @@
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/setup/required.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",

View File

@ -28,6 +28,11 @@
"comment": "Storage used for account management.",
"@id": "urn:solid-server:default:AccountStorage",
"@type": "MemoryMapStorage"
},
{
"comment": "Storage used by setup components.",
"@id": "urn:solid-server:default:SetupStorage",
"@type": "MemoryMapStorage"
}
]
}

View File

@ -47,6 +47,14 @@
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/.internal/accounts/"
},
{
"comment": "Storage used by setup components.",
"@id": "urn:solid-server:default:SetupStorage",
"@type": "JsonResourceStorage",
"source": { "@id": "urn:solid-server:default:ResourceStore" },
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"container": "/.internal/setup/"
},
{
"comment": "Block external access to the storage containers to avoid exposing internal data.",
"@id": "urn:solid-server:default:PathBasedAuthorizer",

View File

@ -317,6 +317,7 @@ export * from './util/handlers/BooleanHandler';
export * from './util/handlers/ConditionalHandler';
export * from './util/handlers/ParallelHandler';
export * from './util/handlers/SequenceHandler';
export * from './util/handlers/StaticHandler';
export * from './util/handlers/UnsupportedAsyncHandler';
export * from './util/handlers/WaterfallHandler';

View File

@ -0,0 +1,22 @@
import { AsyncHandler } from './AsyncHandler';
/**
* A handler that always resolves and always returns the stored value.
* Will return undefined if no value is stored.
*
* The generic type extends `any` due to Components.js requirements.
*/
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
export class StaticHandler<T extends any = void> extends AsyncHandler<any, T> {
private readonly value?: T;
public constructor(value?: T) {
super();
this.value = value;
}
public async handle(): Promise<T> {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return this.value!;
}
}

View File

@ -0,0 +1,203 @@
<% const isBlankForm = !('prefilled' in locals); %>
<% prefilled = locals.prefilled || {}; %>
<fieldset>
<legend>Your WebID</legend>
<p>
A <em>WebID</em> is a unique identifier for you
in the form of a URL.
<br>
You WebID lets you log in to Solid apps
and access non-public data in Pods.
</p>
<ol>
<li class="radio">
<label>
<input type="radio" id="createWebIdOn" name="createWebId" value="on"<%
if (isBlankForm || prefilled.createWebId) { %> checked<% } %>>
Create a new WebID for my Pod.
</label>
<p id="createWebIdForm">
Please also create a Pod below, since your WebID will be stored there.
</p>
</li>
<li class="radio">
<label>
<input type="radio" id="createWebIdOff" name="createWebId" value=""<%
if (!isBlankForm && !prefilled.createWebId) { %> checked<% } %>>
Use my existing WebID to access my Pod.
</label>
<ol id="existingWebIdForm">
<li>
<label for="webId">Existing WebID:</label>
<input id="webId" type="text" name="webId" value="<%= prefilled.webId || '' %>">
</li>
<li class="checkbox">
<label>
<input type="checkbox" id="register" name="register"<%
if (isBlankForm || prefilled.register) { %> checked<% } %>>
Use my new account to log in with this WebID.
</label>
</li>
</ol>
</li>
</ol>
</fieldset>
<fieldset>
<legend>Your Pod</legend>
<p>
A Pod is a place to store your data.
<br>
If you create a new WebID, you must also create a Pod to store that WebID.
</p>
<ol>
<li class="checkbox">
<label>
<input type="checkbox" id="createPod" name="createPod"<%
if (isBlankForm || prefilled.createPod) { %> checked<% } %>>
Create a new Pod with my WebID as owner
</label>
<ol id="createPodForm">
<li class="radio" id="rootPodOnForm">
<label>
<input type="radio" id="rootPodOn" name="rootPod" value="on"<%
if (locals.allowRoot && (isBlankForm || prefilled.rootPod)) { %> checked<% } %>>
... in the root.
</label>
</li>
<li class="radio">
<label>
<input type="radio" id="rootPodOff" name="rootPod" value=""<%
if (!locals.allowRoot || (!isBlankForm && !prefilled.rootPod)) { %> checked<% } %>>
... in its own namespace.
</label>
<ol id="podNameForm">
<li>
<label for="podName">Pod name:</label>
<input id="podName" type="text" name="podName" value="<%= prefilled.podName || '' %>">
</li>
</ol>
</li>
</ol>
</li>
</ol>
</fieldset>
<fieldset>
<legend>Your account</legend>
<div>
<p>
Choose the credentials you want to use to log in to this server in the future.
</p>
<ol>
<li>
<label for="email">Email:</label>
<input id="email" type="text" name="email" value="<%= prefilled.email || '' %>" >
</li>
</ol>
<ol id="passwordForm">
<li>
<label for="password">Password:</label>
<input id="password" type="password" name="password">
</li>
<li>
<label for="confirmPassword">Confirm password:</label>
<input id="confirmPassword" type="password" name="confirmPassword">
</li>
</ol>
</div>
<div id="noPasswordForm" class="hidden">
<p>
Since you will be using your existing WebID setup to access your pod,
<br>
you do <em>not</em> need to set a password.
</p>
</div>
</fieldset>
<script>
// Assist the user with filling out the form by hiding irrelevant fields
(() => {
// Wire up the UI elements
const elements = {};
[
'createWebIdOn', 'createWebIdOff', 'createWebIdForm', 'existingWebIdForm', 'webId',
'createPod', 'createPodForm', 'rootPodOnForm', 'rootPodOn', 'rootPodOff', 'podNameForm', 'podName',
'register', 'passwordForm', 'noPasswordForm',
].forEach(id => {
elements[id] = document.getElementById(id);
elements[id].addEventListener('change', updateUI);
});
elements.mainForm = document.getElementById('<%= formId %>');
updateUI();
// Updates the UI when something has changed
function updateUI({ srcElement } = {}) {
// When Pod creation is required, automatically tick the corresponding checkbox
if (elements.createWebIdOn.checked)
elements.createPod.checked = true;
elements.createPod.disabled = elements.createWebIdOn.checked;
// Hide irrelevant fields
setVisibility('createWebIdForm', elements.createWebIdOn.checked);
setVisibility('existingWebIdForm', elements.createWebIdOff.checked);
setVisibility('createPodForm', elements.createPod.checked);
setVisibility('rootPodOnForm', <%= locals.allowRoot %>);
setVisibility('podNameForm', elements.rootPodOff.checked);
setVisibility('passwordForm', elements.createWebIdOn.checked || elements.register.checked);
setVisibility('noPasswordForm', !isVisible('passwordForm'));
// If child elements have just been activated, focus on them
if (srcElement?.checked) {
switch(document.activeElement) {
case elements.createWebIdOff:
const { webId } = elements;
webId.value = webId.value.startsWith('http') ? webId.value : 'https://';
webId.focus();
break;
case elements.createPod:
if (elements.rootPodOn.checked) {
break;
}
case elements.rootPodOff:
elements.podName.focus();
break;
}
}
}
// Checks whether the given element is visible
function isVisible(element) {
return !(elements[element] ?? element).classList.contains('hidden');
}
// Sets the visibility of the given element
function setVisibility(element, visible) {
// Show or hide the element
element = elements[element] ?? element;
element.classList[visible ? 'remove' : 'add']('hidden');
// Disable children of hidden elements,
// such that the browser does not expect input for them
for (const child of getDescendants(element)) {
if ('disabled' in child)
child.disabled = !visible;
}
}
// Obtains all children, grandchildren, etc. of the given element
function getDescendants(element) {
return [...element.querySelectorAll("*")];
}
// TODO: take form id as input?
// Enable all elements on form submission (otherwise their value is not submitted)
elements.mainForm.addEventListener('submit', () => {
for (const child of getDescendants(elements.mainForm))
child.disabled = false;
});
elements.mainForm.addEventListener('formdata', updateUI);
})();
</script>

View File

@ -0,0 +1,38 @@
<% if (createPod) { %>
<h2>Your new Pod</h2>
<p>
Your new Pod is located at <a href="<%= podBaseUrl %>" class="link"><%= podBaseUrl %></a>.
<br>
You can store your documents and data there.
</p>
<% } %>
<% if (createWebId) { %>
<h2>Your new WebID</h2>
<p>
Your new WebID is <a href="<%= webId %>" class="link"><%= webId %></a>.
<br>
You can use this identifier to interact with Solid pods and apps.
</p>
<% } %>
<% if (register) { %>
<h2>Your new account</h2>
<p>
Via your email address <em><%= email %></em>,
<% if (authenticating) { %>
you can now <a href="<%= controls.login %>">log in</a>
<% } else { %>
this server lets you log in to Solid apps
<% } %>
with your WebID <a href="<%= webId %>" class="link"><%= webId %></a>
</p>
<% if (!createWebId) { %>
<p>
You will need to add the triple
<code><%= `<${webId}> <http://www.w3.org/ns/solid/terms#oidcIssuer> <${oidcIssuer}>.`%></code>
to your existing WebID document <em><%= webId %></em>
to indicate that you trust this server as a login provider.
</p>
<% } %>
<% } %>

View File

@ -4,41 +4,4 @@
We wish you an exciting experience!
</p>
<% if (createPod) { %>
<h2>Your new Pod</h2>
<p>
Your new Pod is located at <a href="<%= podBaseUrl %>" class="link"><%= podBaseUrl %></a>.
<br>
You can store your documents and data there.
</p>
<% } %>
<% if (createWebId) { %>
<h2>Your new WebID</h2>
<p>
Your new WebID is <a href="<%= webId %>" class="link"><%= webId %></a>.
<br>
You can use this identifier to interact with Solid pods and apps.
</p>
<% } %>
<% if (register) { %>
<h2>Your new account</h2>
<p>
Via your email address <em><%= email %></em>,
<% if (authenticating) { %>
you can now <a href="<%= controls.login %>">log in</a>
<% } else { %>
this server lets you log in to Solid apps
<% } %>
with your WebID <a href="<%= webId %>" class="link"><%= webId %></a>
</p>
<% if (!createWebId) { %>
<p>
You will need to add the triple
<code><%= `<${webId}> <http://www.w3.org/ns/solid/terms#oidcIssuer> <${oidcIssuer}>.`%></code>
to your existing WebID document <em><%= webId %></em>
to indicate that you trust this server as a login provider.
</p>
<% } %>
<% } %>
<%- include('./register-response-partial.html.ejs') %>

View File

@ -1,188 +1,11 @@
<h1>Sign up</h1>
<form method="post" id="mainForm">
<% const isBlankForm = !('prefilled' in locals); %>
<% prefilled = locals.prefilled || {}; %>
<% if (locals.message) { %>
<p class="error">Error: <%= message %></p>
<% } %>
<fieldset>
<legend>Your WebID</legend>
<p>
A <em>WebID</em> is a unique identifier for you
in the form of a URL.
<br>
You WebID lets you log in to Solid apps
and access non-public data in Pods.
</p>
<ol>
<li class="radio">
<label>
<input type="radio" id="createWebIdOn" name="createWebId" value="on"<%
if (isBlankForm || prefilled.createWebId) { %> checked<% } %>>
Create a new WebID for my Pod.
</label>
<p id="createWebIdForm">
Please also create a Pod below, since your WebID will be stored there.
</p>
</li>
<li class="radio">
<label>
<input type="radio" id="createWebIdOff" name="createWebId" value=""<%
if (!isBlankForm && !prefilled.createWebId) { %> checked<% } %>>
Use my existing WebID to access my Pod.
</label>
<ol id="existingWebIdForm">
<li>
<label for="webId">Existing WebID:</label>
<input id="webId" type="text" name="webId" value="<%= prefilled.webId || '' %>">
</li>
<li class="checkbox">
<label>
<input type="checkbox" id="register" name="register"<%
if (isBlankForm || prefilled.register) { %> checked<% } %>>
Use my new account to log in with this WebID.
</label>
</li>
</ol>
</li>
</ol>
</fieldset>
<fieldset>
<legend>Your Pod</legend>
<p>
A Pod is a place to store your data.
<br>
If you create a new WebID, you must also create a Pod to store that WebID.
</p>
<ol>
<li class="checkbox">
<label>
<input type="checkbox" id="createPod" name="createPod"<%
if (isBlankForm || prefilled.createPod) { %> checked<% } %>>
Create a new Pod with my WebID as owner.
</label>
<ol id="createPodForm">
<li>
<label for="podName">Pod name:</label>
<input id="podName" type="text" name="podName" value="<%= prefilled.podName || '' %>">
</li>
</ol>
</li>
</ol>
</fieldset>
<fieldset>
<legend>Your account</legend>
<div>
<p>
Choose the credentials you want to use to log in to this server in the future.
</p>
<ol>
<li>
<label for="email">Email:</label>
<input id="email" type="text" name="email" value="<%= prefilled.email || '' %>" >
</li>
</ol>
<ol id="passwordForm">
<li>
<label for="password">Password:</label>
<input id="password" type="password" name="password">
</li>
<li>
<label for="confirmPassword">Confirm password:</label>
<input id="confirmPassword" type="password" name="confirmPassword">
</li>
</ol>
</div>
<div id="noPasswordForm" class="hidden">
<p>
Since you will be using your existing WebID setup to access your pod,
<br>
you do <em>not</em> need to set a password.
</p>
</div>
</fieldset>
<%- include('./register-partial.html.ejs', { allowRoot: false, formId: 'mainForm' }) %>
<p class="actions"><button type="submit" name="submit">Sign up</button></p>
</form>
<script>
// Assist the user with filling out the form by hiding irrelevant fields
(() => {
// Wire up the UI elements
const elements = {};
[
'createWebIdOn', 'createWebIdOff', 'createWebIdForm', 'existingWebIdForm', 'webId',
'createPod', 'createPodForm', 'podName',
'register', 'passwordForm', 'noPasswordForm', 'mainForm',
].forEach(id => {
elements[id] = document.getElementById(id);
elements[id].addEventListener('change', updateUI);
});
updateUI();
mainForm.classList.add('loaded');
// Updates the UI when something has changed
function updateUI({ srcElement } = {}) {
// When Pod creation is required, automatically tick the corresponding checkbox
if (elements.createWebIdOn.checked)
elements.createPod.checked = true;
elements.createPod.disabled = elements.createWebIdOn.checked;
// Hide irrelevant fields
setVisibility('createWebIdForm', elements.createWebIdOn.checked);
setVisibility('existingWebIdForm', elements.createWebIdOff.checked);
setVisibility('createPodForm', elements.createPod.checked);
setVisibility('passwordForm', elements.createWebIdOn.checked || elements.register.checked);
setVisibility('noPasswordForm', !isVisible('passwordForm'));
// If child elements have just been activated, focus on them
if (srcElement?.checked) {
switch(document.activeElement) {
case elements.createWebIdOff:
const { webId } = elements;
webId.value = webId.value.startsWith('http') ? webId.value : 'https://';
webId.focus();
break;
case elements.createPod:
elements.podName.focus();
break;
}
}
}
// Checks whether the given element is visible
function isVisible(element) {
return !(elements[element] ?? element).classList.contains('hidden');
}
// Sets the visibility of the given element
function setVisibility(element, visible) {
// Show or hide the element
element = elements[element] ?? element;
element.classList[visible ? 'remove' : 'add']('hidden');
// Disable children of hidden elements,
// such that the browser does not expect input for them
for (const child of getDescendants(element)) {
if ('disabled' in child)
child.disabled = !visible;
}
}
// Obtains all children, grandchildren, etc. of the given element
function getDescendants(element) {
return [...element.querySelectorAll("*")];
}
// Enable all elements on form submission (otherwise their value is not submitted)
elements.mainForm.addEventListener('submit', () => {
for (const child of getDescendants(elements.mainForm))
child.disabled = false;
});
elements.mainForm.addEventListener('formdata', updateUI);
})();
</script>

View File

@ -1,13 +1,10 @@
# Root ACL resource generated by the Community Server to allow public access
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
<#authorization>
a acl:Authorization;
acl:agentClass foaf:Agent;
acl:mode acl:Read;
acl:mode acl:Write;
acl:mode acl:Append;
acl:mode acl:Delete;
acl:mode acl:Control;
acl:mode acl:Read, acl:Write, acl:Append, acl:Control;
acl:accessTo <./>;
acl:default <./>.

View File

@ -0,0 +1,10 @@
# Root ACL resource generated by the Community Server to allow public access
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
<#authorization>
a acl:Authorization;
acl:agentClass foaf:Agent;
acl:mode acl:Read, acl:Write, acl:Append, acl:Control;
acl:accessTo <./>;
acl:default <./>.

View File

@ -0,0 +1,7 @@
@prefix pim: <http://www.w3.org/ns/pim/space#>.
# It is imperative the root container is marked as a pim:Storage :
# Solid, §4.1: "Servers exposing the storage resource MUST advertise by including the HTTP Link header
# with rel="type" targeting http://www.w3.org/ns/pim/space#Storage when responding to storages request URI."
# https://solid.github.io/specification/protocol#storage
<> a pim:Storage.

View File

@ -59,7 +59,7 @@
</li>
<li>
Prevent public write and control access to the root Pod
by modifying <a href=".acl"><code>.acl</code></a>.
by modifying <a href="../../../.acl"><code>.acl</code></a>.
</li>
<li>
Disable Pod registration

View File

@ -0,0 +1,88 @@
<h1>Welcome to Solid</h1>
<p>
This server implements
the <a href="https://solid.github.io/specification/protocol">Solid protocol</a>
so you can create your own <a href="https://solidproject.org/about">Solid Pod</a>
and identity.
</p>
<h2 id="public">Making this server public</h2>
<p>
Before making this server public,
you might want to <strong>disable Pod registration</strong>
by <a href="https://github.com/solid/community-server/blob/main/config/identity/README.md">changing
the configuration</a>.
</p>
<h2 id="setup">Setting up the server</h2>
<p>
The <em>default</em> configuration stores data only in memory,
so be sure to choose a configuration that saves data to disk.
If you are exposing this server publicly,
<a href="#public">read the guidelines below</a>.
</p>
<p>
When using the file-based version of the server,
you can easily choose any folder on your disk to use as root.
<br>
Use the <code>--help</code> switch to learn more.
</p>
<p>
To make sure the server is set up exactly as you want it,
please fill in the form below.
</p>
<p>
In case you want to automate the server initialization and want to get rid of this setup screen,
update your config with new imports from <code>config/app/setup/</code> and possibly <code>config/app/init/</code>.
</p>
<form method="post" id="mainForm">
<% const safePrefilled = locals.prefilled || {}; %>
<% if (locals.message) { %>
<p class="error"><%= message %></p>
<% } %>
<fieldset>
<legend>Choose options</legend>
<ol>
<li class="checkbox">
<label>
<input type="checkbox" id="initialize" name="initialize" checked>
Allow access to the root container.
</label>
<p>
This defaults to public access for everyone.
Disabling this makes it impossible to access the root container and add resources,
but new pods can still be created through registration,
which is ideal if you only want data to be edited in the pods.
This option is irrelevant when creating a root pod with the option below.
</p>
</li>
<li class="checkbox">
<label>
<input type="checkbox" id="registration" name="registration" checked>
Provision a pod, create a WebID, and/or register an identity.
</label>
</li>
</ol>
</fieldset>
<fieldset id="registrationForm">
<%- include('../identity/email-password/register-partial.html.ejs', { allowRoot: true, formId: 'mainForm' }) %>
</fieldset>
<p class="actions"><button type="submit" name="submit">Submit</button></p>
</form>
<script>
const registrationCheckbox = document.getElementById('registration');
registrationCheckbox.addEventListener('change', updateUI);
const registrationForm = document.getElementById('registrationForm');
function updateUI() {
const visible = registrationCheckbox.checked;
registrationForm.classList[visible ? 'remove' : 'add']('hidden');
}
updateUI();
</script>

View File

@ -0,0 +1,24 @@
<h1 id="public">Server successfully set up</h1>
<% if (initialize && !registration) { %>
<p>
You have chosen to allow the root container to be accessible.
Prevent public write and control access to the root
by modifying <a href=".acl"><code>.acl</code></a>.
</p>
<% } %>
<% if (registration) { %>
<%- include('../identity/email-password/register-response-partial.html.ejs', { authenticating: false }) %>
<% } %>
<h2>Have a wonderful Solid experience</h2>
<p>
<strong>Learn more about Solid
at <a href="https://solidproject.org/">solidproject.org</a>.</strong>
</p>
<p>
You are warmly invited
to <a href="https://github.com/solid/community-server/discussions">share your experiences</a>
and to <a href="https://github.com/solid/community-server/issues">report any bugs</a> you encounter.
</p>

View File

@ -72,13 +72,6 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`);
});
it('can read the root container index page when asking for HTML.', async(): Promise<void> => {
const response = await getResource(baseUrl, { accept: 'text/html' }, { contentType: 'text/html' });
await expect(response.text()).resolves.toContain('Welcome to Solid');
expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`);
});
it('can read a container listing with a query string.', async(): Promise<void> => {
// Helper functions would fail due to query params
const response = await fetch(`${baseUrl}?abc=def&xyz`, { headers: { accept: 'text/turtle' }});
@ -92,9 +85,6 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
const quads = parser.parse(await response.text());
const store = new Store(quads);
expect(store.countQuads(namedNode(baseUrl), RDF.terms.type, LDP.terms.Container, null)).toBe(1);
const contains = store.getObjects(namedNode(baseUrl), LDP.terms.contains, null);
expect(contains).toHaveLength(1);
expect(contains[0].value).toBe(`${baseUrl}index.html`);
});
it('can add a document to the store, read it and delete it.', async(): Promise<void> => {

View File

@ -0,0 +1,117 @@
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);
await expect(res.text()).resolves.toContain('Welcome to Solid');
res = await fetch(joinUrl(baseUrl, '/random/path/'), { method: 'GET', headers: { accept: 'text/html' }});
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain('Welcome to Solid');
res = await fetch(joinUrl(baseUrl, '/random/path/'), { method: 'PUT', headers: { accept: 'text/html' }});
expect(res.status).toBe(405);
await expect(res.text()).resolves.toContain('Welcome to Solid');
});
it('can create a server that disables root but allows registration.', async(): Promise<void> => {
let res = await fetch(setupUrl, { method: 'POST', headers: { accept: 'text/html' }});
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain('Server successfully set up');
// Root access disabled
res = await fetch(baseUrl);
expect(res.status).toBe(403);
// Registration still possible
const registerParams = { email, podName, password, confirmPassword: password, createWebId: true };
res = await fetch(joinUrl(baseUrl, 'idp/register'), {
method: 'POST',
headers: { accept: 'text/html', '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: { accept: 'text/html', 'content-type': 'application/json' },
body: JSON.stringify({ initialize: true }),
});
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain('Server successfully set up');
// 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: { accept: 'text/html', '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: { accept: 'text/html', 'content-type': 'application/json' },
body: JSON.stringify({ registration: true, initialize: true, ...registerParams }),
});
expect(res.status).toBe(200);
await expect(res.text()).resolves.toContain('Server successfully set up');
// 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: { accept: 'text/html', 'content-type': 'text/plain' },
body: 'random data',
});
expect(res.status).toBe(401);
});
});

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/simple.json",
"files-scs:config/http/middleware/no-websockets.json",
"files-scs:config/http/server-factory/no-websockets.json",

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/simple.json",
"files-scs:config/http/middleware/no-websockets.json",
"files-scs:config/http/server-factory/no-websockets.json",

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/no-websockets.json",
"files-scs:config/http/server-factory/no-websockets.json",

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/no-websockets.json",
"files-scs:config/http/server-factory/no-websockets.json",

View File

@ -2,7 +2,8 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/app/main/default.json",
"files-scs:config/app/init/default.json",
"files-scs:config/app/init/initialize-root.json",
"files-scs:config/app/setup/disabled.json",
"files-scs:config/http/handler/default.json",
"files-scs:config/http/middleware/websockets.json",
"files-scs:config/http/server-factory/websockets.json",

View File

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

View File

@ -0,0 +1,18 @@
import { StaticHandler } from '../../../../src/util/handlers/StaticHandler';
describe('A StaticHandler', (): void => {
it('can handle everything.', async(): Promise<void> => {
const handler = new StaticHandler();
await expect(handler.canHandle(null)).resolves.toBeUndefined();
});
it('returns the stored value.', async(): Promise<void> => {
const handler = new StaticHandler('apple');
await expect(handler.handle()).resolves.toEqual('apple');
});
it('returns undefined if there is no stored value.', async(): Promise<void> => {
const handler = new StaticHandler();
await expect(handler.handle()).resolves.toBeUndefined();
});
});

View File

@ -13,6 +13,7 @@ const portNames = [
'PodCreation',
'RedisResourceLocker',
'ServerFetch',
'SetupMemory',
'SparqlStorage',
'Subdomains',
'WebSocketsProtocol',