mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add migration for v6 account data
This commit is contained in:
@@ -8,7 +8,7 @@ import { ACCOUNT_SETTINGS_REMEMBER_LOGIN } from './AccountStore';
|
||||
import type { AccountLoginStorage } from './LoginStorage';
|
||||
import { ACCOUNT_TYPE } from './LoginStorage';
|
||||
|
||||
const STORAGE_DESCRIPTION = {
|
||||
export const ACCOUNT_STORAGE_DESCRIPTION = {
|
||||
[ACCOUNT_SETTINGS_REMEMBER_LOGIN]: 'boolean?',
|
||||
} as const;
|
||||
|
||||
@@ -19,7 +19,7 @@ const STORAGE_DESCRIPTION = {
|
||||
export class BaseAccountStore extends Initializer implements AccountStore {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly storage: AccountLoginStorage<{ [ACCOUNT_TYPE]: typeof STORAGE_DESCRIPTION }>;
|
||||
private readonly storage: AccountLoginStorage<{ [ACCOUNT_TYPE]: typeof ACCOUNT_STORAGE_DESCRIPTION }>;
|
||||
private initialized = false;
|
||||
|
||||
public constructor(storage: AccountLoginStorage<any>) {
|
||||
@@ -33,7 +33,7 @@ export class BaseAccountStore extends Initializer implements AccountStore {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.storage.defineType(ACCOUNT_TYPE, STORAGE_DESCRIPTION, false);
|
||||
await this.storage.defineType(ACCOUNT_TYPE, ACCOUNT_STORAGE_DESCRIPTION, false);
|
||||
this.initialized = true;
|
||||
} catch (cause: unknown) {
|
||||
throw new InternalServerError(`Error defining account in storage: ${createErrorMessage(cause)}`, { cause });
|
||||
@@ -58,6 +58,6 @@ export class BaseAccountStore extends Initializer implements AccountStore {
|
||||
|
||||
public async updateSetting<T extends keyof AccountSettings>(id: string, setting: T, value: AccountSettings[T]):
|
||||
Promise<void> {
|
||||
await this.storage.setField(ACCOUNT_TYPE, id, setting, value as ValueType<typeof STORAGE_DESCRIPTION[T]>);
|
||||
await this.storage.setField(ACCOUNT_TYPE, id, setting, value as ValueType<typeof ACCOUNT_STORAGE_DESCRIPTION[T]>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { ACCOUNT_TYPE } from '../../account/util/LoginStorage';
|
||||
import type { AccountLoginStorage } from '../../account/util/LoginStorage';
|
||||
import type { ClientCredentials, ClientCredentialsStore } from './ClientCredentialsStore';
|
||||
|
||||
const STORAGE_TYPE = 'clientCredentials';
|
||||
const STORAGE_DESCRIPTION = {
|
||||
export const CLIENT_CREDENTIALS_STORAGE_TYPE = 'clientCredentials';
|
||||
export const CLIENT_CREDENTIALS_STORAGE_DESCRIPTION = {
|
||||
label: 'string',
|
||||
accountId: `id:${ACCOUNT_TYPE}`,
|
||||
secret: 'string',
|
||||
@@ -22,7 +22,9 @@ const STORAGE_DESCRIPTION = {
|
||||
export class BaseClientCredentialsStore extends Initializer implements ClientCredentialsStore {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly storage: AccountLoginStorage<{ [STORAGE_TYPE]: typeof STORAGE_DESCRIPTION }>;
|
||||
private readonly storage: AccountLoginStorage<{ [CLIENT_CREDENTIALS_STORAGE_TYPE]:
|
||||
typeof CLIENT_CREDENTIALS_STORAGE_DESCRIPTION; }>;
|
||||
|
||||
private initialized = false;
|
||||
|
||||
public constructor(storage: AccountLoginStorage<any>) {
|
||||
@@ -36,9 +38,9 @@ export class BaseClientCredentialsStore extends Initializer implements ClientCre
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.storage.defineType(STORAGE_TYPE, STORAGE_DESCRIPTION, false);
|
||||
await this.storage.createIndex(STORAGE_TYPE, 'accountId');
|
||||
await this.storage.createIndex(STORAGE_TYPE, 'label');
|
||||
await this.storage.defineType(CLIENT_CREDENTIALS_STORAGE_TYPE, CLIENT_CREDENTIALS_STORAGE_DESCRIPTION, false);
|
||||
await this.storage.createIndex(CLIENT_CREDENTIALS_STORAGE_TYPE, 'accountId');
|
||||
await this.storage.createIndex(CLIENT_CREDENTIALS_STORAGE_TYPE, 'label');
|
||||
this.initialized = true;
|
||||
} catch (cause: unknown) {
|
||||
throw new InternalServerError(`Error defining client credentials in storage: ${createErrorMessage(cause)}`,
|
||||
@@ -47,11 +49,11 @@ export class BaseClientCredentialsStore extends Initializer implements ClientCre
|
||||
}
|
||||
|
||||
public async get(id: string): Promise<ClientCredentials | undefined> {
|
||||
return this.storage.get(STORAGE_TYPE, id);
|
||||
return this.storage.get(CLIENT_CREDENTIALS_STORAGE_TYPE, id);
|
||||
}
|
||||
|
||||
public async findByLabel(label: string): Promise<ClientCredentials | undefined> {
|
||||
const result = await this.storage.find(STORAGE_TYPE, { label });
|
||||
const result = await this.storage.find(CLIENT_CREDENTIALS_STORAGE_TYPE, { label });
|
||||
if (result.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -59,7 +61,7 @@ export class BaseClientCredentialsStore extends Initializer implements ClientCre
|
||||
}
|
||||
|
||||
public async findByAccount(accountId: string): Promise<ClientCredentials[]> {
|
||||
return this.storage.find(STORAGE_TYPE, { accountId });
|
||||
return this.storage.find(CLIENT_CREDENTIALS_STORAGE_TYPE, { accountId });
|
||||
}
|
||||
|
||||
public async create(label: string, webId: string, accountId: string): Promise<ClientCredentials> {
|
||||
@@ -69,11 +71,11 @@ export class BaseClientCredentialsStore extends Initializer implements ClientCre
|
||||
`Creating client credentials token with label ${label} for WebID ${webId} and account ${accountId}`,
|
||||
);
|
||||
|
||||
return this.storage.create(STORAGE_TYPE, { accountId, label, webId, secret });
|
||||
return this.storage.create(CLIENT_CREDENTIALS_STORAGE_TYPE, { accountId, label, webId, secret });
|
||||
}
|
||||
|
||||
public async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting client credentials token with ID ${id}`);
|
||||
return this.storage.delete(STORAGE_TYPE, id);
|
||||
return this.storage.delete(CLIENT_CREDENTIALS_STORAGE_TYPE, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { ACCOUNT_TYPE } from '../../account/util/LoginStorage';
|
||||
import type { AccountLoginStorage } from '../../account/util/LoginStorage';
|
||||
import type { PasswordStore } from './PasswordStore';
|
||||
|
||||
const STORAGE_TYPE = 'password';
|
||||
const STORAGE_DESCRIPTION = {
|
||||
export const PASSWORD_STORAGE_TYPE = 'password';
|
||||
export const PASSWORD_STORAGE_DESCRIPTION = {
|
||||
email: 'string',
|
||||
password: 'string',
|
||||
verified: 'boolean',
|
||||
@@ -25,7 +25,7 @@ const STORAGE_DESCRIPTION = {
|
||||
export class BasePasswordStore extends Initializer implements PasswordStore {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly storage: AccountLoginStorage<{ [STORAGE_TYPE]: typeof STORAGE_DESCRIPTION }>;
|
||||
private readonly storage: AccountLoginStorage<{ [PASSWORD_STORAGE_TYPE]: typeof PASSWORD_STORAGE_DESCRIPTION }>;
|
||||
private readonly saltRounds: number;
|
||||
private initialized = false;
|
||||
|
||||
@@ -41,9 +41,9 @@ export class BasePasswordStore extends Initializer implements PasswordStore {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.storage.defineType(STORAGE_TYPE, STORAGE_DESCRIPTION, true);
|
||||
await this.storage.createIndex(STORAGE_TYPE, 'accountId');
|
||||
await this.storage.createIndex(STORAGE_TYPE, 'email');
|
||||
await this.storage.defineType(PASSWORD_STORAGE_TYPE, PASSWORD_STORAGE_DESCRIPTION, true);
|
||||
await this.storage.createIndex(PASSWORD_STORAGE_TYPE, 'accountId');
|
||||
await this.storage.createIndex(PASSWORD_STORAGE_TYPE, 'email');
|
||||
this.initialized = true;
|
||||
} catch (cause: unknown) {
|
||||
throw new InternalServerError(`Error defining email/password in storage: ${createErrorMessage(cause)}`,
|
||||
@@ -56,7 +56,7 @@ export class BasePasswordStore extends Initializer implements PasswordStore {
|
||||
this.logger.warn(`Trying to create duplicate login for email ${email}`);
|
||||
throw new BadRequestHttpError('There already is a login for this e-mail address.');
|
||||
}
|
||||
const payload = await this.storage.create(STORAGE_TYPE, {
|
||||
const payload = await this.storage.create(PASSWORD_STORAGE_TYPE, {
|
||||
accountId,
|
||||
email: email.toLowerCase(),
|
||||
password: await hash(password, this.saltRounds),
|
||||
@@ -66,7 +66,7 @@ export class BasePasswordStore extends Initializer implements PasswordStore {
|
||||
}
|
||||
|
||||
public async get(id: string): Promise<{ email: string; accountId: string } | undefined> {
|
||||
const result = await this.storage.get(STORAGE_TYPE, id);
|
||||
const result = await this.storage.get(PASSWORD_STORAGE_TYPE, id);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export class BasePasswordStore extends Initializer implements PasswordStore {
|
||||
}
|
||||
|
||||
public async findByEmail(email: string): Promise<{ accountId: string; id: string } | undefined> {
|
||||
const payload = await this.storage.find(STORAGE_TYPE, { email: email.toLowerCase() });
|
||||
const payload = await this.storage.find(PASSWORD_STORAGE_TYPE, { email: email.toLowerCase() });
|
||||
if (payload.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -82,21 +82,21 @@ export class BasePasswordStore extends Initializer implements PasswordStore {
|
||||
}
|
||||
|
||||
public async findByAccount(accountId: string): Promise<{ id: string; email: string }[]> {
|
||||
return (await this.storage.find(STORAGE_TYPE, { accountId }))
|
||||
return (await this.storage.find(PASSWORD_STORAGE_TYPE, { accountId }))
|
||||
.map(({ id, email }): { id: string; email: string } => ({ id, email }));
|
||||
}
|
||||
|
||||
public async confirmVerification(id: string): Promise<void> {
|
||||
if (!await this.storage.has(STORAGE_TYPE, id)) {
|
||||
if (!await this.storage.has(PASSWORD_STORAGE_TYPE, id)) {
|
||||
this.logger.warn(`Trying to verify unknown password login ${id}`);
|
||||
throw new ForbiddenHttpError('Login does not exist.');
|
||||
}
|
||||
|
||||
await this.storage.setField(STORAGE_TYPE, id, 'verified', true);
|
||||
await this.storage.setField(PASSWORD_STORAGE_TYPE, id, 'verified', true);
|
||||
}
|
||||
|
||||
public async authenticate(email: string, password: string): Promise<{ accountId: string; id: string }> {
|
||||
const payload = await this.storage.find(STORAGE_TYPE, { email: email.toLowerCase() });
|
||||
const payload = await this.storage.find(PASSWORD_STORAGE_TYPE, { email: email.toLowerCase() });
|
||||
if (payload.length === 0) {
|
||||
this.logger.warn(`Trying to get account info for unknown email ${email}`);
|
||||
throw new ForbiddenHttpError('Invalid email/password combination.');
|
||||
@@ -114,14 +114,14 @@ export class BasePasswordStore extends Initializer implements PasswordStore {
|
||||
}
|
||||
|
||||
public async update(id: string, password: string): Promise<void> {
|
||||
if (!await this.storage.has(STORAGE_TYPE, id)) {
|
||||
if (!await this.storage.has(PASSWORD_STORAGE_TYPE, id)) {
|
||||
this.logger.warn(`Trying to update unknown password login ${id}`);
|
||||
throw new ForbiddenHttpError('Login does not exist.');
|
||||
}
|
||||
await this.storage.setField(STORAGE_TYPE, id, 'password', await hash(password, this.saltRounds));
|
||||
await this.storage.setField(PASSWORD_STORAGE_TYPE, id, 'password', await hash(password, this.saltRounds));
|
||||
}
|
||||
|
||||
public async delete(id: string): Promise<void> {
|
||||
return this.storage.delete(STORAGE_TYPE, id);
|
||||
return this.storage.delete(PASSWORD_STORAGE_TYPE, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,17 +9,17 @@ import { ACCOUNT_TYPE } from '../../account/util/LoginStorage';
|
||||
import type { AccountLoginStorage } from '../../account/util/LoginStorage';
|
||||
import type { PodStore } from './PodStore';
|
||||
|
||||
const STORAGE_TYPE = 'pod';
|
||||
const STORAGE_DESCRIPTION = {
|
||||
export const POD_STORAGE_TYPE = 'pod';
|
||||
export const POD_STORAGE_DESCRIPTION = {
|
||||
baseUrl: 'string',
|
||||
accountId: `id:${ACCOUNT_TYPE}`,
|
||||
} as const;
|
||||
|
||||
const OWNER_TYPE = 'owner';
|
||||
const OWNER_DESCRIPTION = {
|
||||
export const OWNER_STORAGE_TYPE = 'owner';
|
||||
export const OWNER_STORAGE_DESCRIPTION = {
|
||||
webId: 'string',
|
||||
visible: 'boolean',
|
||||
podId: `id:${STORAGE_TYPE}`,
|
||||
podId: `id:${POD_STORAGE_TYPE}`,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -35,8 +35,8 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly storage: AccountLoginStorage<{
|
||||
[STORAGE_TYPE]: typeof STORAGE_DESCRIPTION;
|
||||
[OWNER_TYPE]: typeof OWNER_DESCRIPTION; }>;
|
||||
[POD_STORAGE_TYPE]: typeof POD_STORAGE_DESCRIPTION;
|
||||
[OWNER_STORAGE_TYPE]: typeof OWNER_STORAGE_DESCRIPTION; }>;
|
||||
|
||||
private readonly manager: PodManager;
|
||||
private readonly visible: boolean;
|
||||
@@ -56,11 +56,11 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.storage.defineType(STORAGE_TYPE, STORAGE_DESCRIPTION, false);
|
||||
await this.storage.createIndex(STORAGE_TYPE, 'accountId');
|
||||
await this.storage.createIndex(STORAGE_TYPE, 'baseUrl');
|
||||
await this.storage.defineType(OWNER_TYPE, OWNER_DESCRIPTION, false);
|
||||
await this.storage.createIndex(OWNER_TYPE, 'podId');
|
||||
await this.storage.defineType(POD_STORAGE_TYPE, POD_STORAGE_DESCRIPTION, false);
|
||||
await this.storage.createIndex(POD_STORAGE_TYPE, 'accountId');
|
||||
await this.storage.createIndex(POD_STORAGE_TYPE, 'baseUrl');
|
||||
await this.storage.defineType(OWNER_STORAGE_TYPE, OWNER_STORAGE_DESCRIPTION, false);
|
||||
await this.storage.createIndex(OWNER_STORAGE_TYPE, 'podId');
|
||||
this.initialized = true;
|
||||
} catch (cause: unknown) {
|
||||
throw new InternalServerError(`Error defining pods in storage: ${createErrorMessage(cause)}`,
|
||||
@@ -71,14 +71,14 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
public async create(accountId: string, settings: PodSettings, overwrite: boolean): Promise<string> {
|
||||
// Adding pod to storage first as we cannot undo creating the pod below.
|
||||
// This call might also fail because there is no login method yet on the account.
|
||||
const pod = await this.storage.create(STORAGE_TYPE, { baseUrl: settings.base.path, accountId });
|
||||
await this.storage.create(OWNER_TYPE, { podId: pod.id, webId: settings.webId, visible: this.visible });
|
||||
const pod = await this.storage.create(POD_STORAGE_TYPE, { baseUrl: settings.base.path, accountId });
|
||||
await this.storage.create(OWNER_STORAGE_TYPE, { podId: pod.id, webId: settings.webId, visible: this.visible });
|
||||
|
||||
try {
|
||||
await this.manager.createPod(settings, overwrite);
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn(`Pod creation failed for account ${accountId}: ${createErrorMessage(error)}`);
|
||||
await this.storage.delete(STORAGE_TYPE, pod.id);
|
||||
await this.storage.delete(POD_STORAGE_TYPE, pod.id);
|
||||
throw new BadRequestHttpError(`Pod creation failed: ${createErrorMessage(error)}`, { cause: error });
|
||||
}
|
||||
this.logger.debug(`Created pod ${settings.name} for account ${accountId}`);
|
||||
@@ -87,7 +87,7 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
}
|
||||
|
||||
public async get(id: string): Promise<{ baseUrl: string; accountId: string } | undefined> {
|
||||
const pod = await this.storage.get(STORAGE_TYPE, id);
|
||||
const pod = await this.storage.get(POD_STORAGE_TYPE, id);
|
||||
if (!pod) {
|
||||
return;
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
}
|
||||
|
||||
public async findByBaseUrl(baseUrl: string): Promise<{ id: string; accountId: string } | undefined> {
|
||||
const result = await this.storage.find(STORAGE_TYPE, { baseUrl });
|
||||
const result = await this.storage.find(POD_STORAGE_TYPE, { baseUrl });
|
||||
if (result.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -103,12 +103,12 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
}
|
||||
|
||||
public async findPods(accountId: string): Promise<{ id: string; baseUrl: string }[]> {
|
||||
return (await this.storage.find(STORAGE_TYPE, { accountId }))
|
||||
return (await this.storage.find(POD_STORAGE_TYPE, { accountId }))
|
||||
.map(({ id, baseUrl }): { id: string; baseUrl: string } => ({ id, baseUrl }));
|
||||
}
|
||||
|
||||
public async getOwners(id: string): Promise<{ webId: string; visible: boolean }[] | undefined> {
|
||||
const results = await this.storage.find(OWNER_TYPE, { podId: id });
|
||||
const results = await this.storage.find(OWNER_STORAGE_TYPE, { podId: id });
|
||||
if (results.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -119,16 +119,16 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
public async updateOwner(id: string, webId: string, visible: boolean): Promise<void> {
|
||||
// Need to first check if there already is an owner with the given WebID
|
||||
// so we know if we need to create or update.
|
||||
const matches = await this.storage.find(OWNER_TYPE, { webId, podId: id });
|
||||
const matches = await this.storage.find(OWNER_STORAGE_TYPE, { webId, podId: id });
|
||||
if (matches.length === 0) {
|
||||
await this.storage.create(OWNER_TYPE, { webId, visible, podId: id });
|
||||
await this.storage.create(OWNER_STORAGE_TYPE, { webId, visible, podId: id });
|
||||
} else {
|
||||
await this.storage.setField(OWNER_TYPE, matches[0].id, 'visible', visible);
|
||||
await this.storage.setField(OWNER_STORAGE_TYPE, matches[0].id, 'visible', visible);
|
||||
}
|
||||
}
|
||||
|
||||
public async removeOwner(id: string, webId: string): Promise<void> {
|
||||
const owners = await this.storage.find(OWNER_TYPE, { podId: id });
|
||||
const owners = await this.storage.find(OWNER_STORAGE_TYPE, { podId: id });
|
||||
const match = owners.find((owner): boolean => owner.webId === webId);
|
||||
if (!match) {
|
||||
return;
|
||||
@@ -136,6 +136,6 @@ export class BasePodStore extends Initializer implements PodStore {
|
||||
if (owners.length === 1) {
|
||||
throw new BadRequestHttpError('Unable to remove the last owner of a pod.');
|
||||
}
|
||||
await this.storage.delete(OWNER_TYPE, match.id);
|
||||
await this.storage.delete(OWNER_STORAGE_TYPE, match.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { ACCOUNT_TYPE } from '../../account/util/LoginStorage';
|
||||
import type { AccountLoginStorage } from '../../account/util/LoginStorage';
|
||||
import type { WebIdStore } from './WebIdStore';
|
||||
|
||||
const STORAGE_TYPE = 'webIdLink';
|
||||
const STORAGE_DESCRIPTION = {
|
||||
export const WEBID_STORAGE_TYPE = 'webIdLink';
|
||||
export const WEBID_STORAGE_DESCRIPTION = {
|
||||
webId: 'string',
|
||||
accountId: `id:${ACCOUNT_TYPE}`,
|
||||
} as const;
|
||||
@@ -20,7 +20,7 @@ const STORAGE_DESCRIPTION = {
|
||||
export class BaseWebIdStore extends Initializer implements WebIdStore {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly storage: AccountLoginStorage<{ [STORAGE_TYPE]: typeof STORAGE_DESCRIPTION }>;
|
||||
private readonly storage: AccountLoginStorage<{ [WEBID_STORAGE_TYPE]: typeof WEBID_STORAGE_DESCRIPTION }>;
|
||||
private initialized = false;
|
||||
|
||||
public constructor(storage: AccountLoginStorage<any>) {
|
||||
@@ -34,9 +34,9 @@ export class BaseWebIdStore extends Initializer implements WebIdStore {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.storage.defineType(STORAGE_TYPE, STORAGE_DESCRIPTION, false);
|
||||
await this.storage.createIndex(STORAGE_TYPE, 'accountId');
|
||||
await this.storage.createIndex(STORAGE_TYPE, 'webId');
|
||||
await this.storage.defineType(WEBID_STORAGE_TYPE, WEBID_STORAGE_DESCRIPTION, false);
|
||||
await this.storage.createIndex(WEBID_STORAGE_TYPE, 'accountId');
|
||||
await this.storage.createIndex(WEBID_STORAGE_TYPE, 'webId');
|
||||
this.initialized = true;
|
||||
} catch (cause: unknown) {
|
||||
throw new InternalServerError(`Error defining WebID links in storage: ${createErrorMessage(cause)}`,
|
||||
@@ -45,16 +45,16 @@ export class BaseWebIdStore extends Initializer implements WebIdStore {
|
||||
}
|
||||
|
||||
public async get(id: string): Promise<{ accountId: string; webId: string } | undefined> {
|
||||
return this.storage.get(STORAGE_TYPE, id);
|
||||
return this.storage.get(WEBID_STORAGE_TYPE, id);
|
||||
}
|
||||
|
||||
public async isLinked(webId: string, accountId: string): Promise<boolean> {
|
||||
const result = await this.storage.find(STORAGE_TYPE, { webId, accountId });
|
||||
const result = await this.storage.find(WEBID_STORAGE_TYPE, { webId, accountId });
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
public async findLinks(accountId: string): Promise<{ id: string; webId: string }[]> {
|
||||
return (await this.storage.find(STORAGE_TYPE, { accountId }))
|
||||
return (await this.storage.find(WEBID_STORAGE_TYPE, { accountId }))
|
||||
.map(({ id, webId }): { id: string; webId: string } => ({ id, webId }));
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export class BaseWebIdStore extends Initializer implements WebIdStore {
|
||||
throw new BadRequestHttpError(`${webId} is already registered to this account.`);
|
||||
}
|
||||
|
||||
const result = await this.storage.create(STORAGE_TYPE, { webId, accountId });
|
||||
const result = await this.storage.create(WEBID_STORAGE_TYPE, { webId, accountId });
|
||||
|
||||
this.logger.debug(`Linked WebID ${webId} to account ${accountId}`);
|
||||
|
||||
@@ -73,6 +73,6 @@ export class BaseWebIdStore extends Initializer implements WebIdStore {
|
||||
|
||||
public async delete(linkId: string): Promise<void> {
|
||||
this.logger.debug(`Deleting WebID link with ID ${linkId}`);
|
||||
return this.storage.delete(STORAGE_TYPE, linkId);
|
||||
return this.storage.delete(WEBID_STORAGE_TYPE, linkId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +275,10 @@ export * from './init/final/Finalizer';
|
||||
export * from './init/cli/CliExtractor';
|
||||
export * from './init/cli/YargsCliExtractor';
|
||||
|
||||
// Init/Migration
|
||||
export * from './init/migration/SingleContainerJsonStorage';
|
||||
export * from './init/migration/V6MigrationInitializer';
|
||||
|
||||
// Init/Variables/Extractors
|
||||
export * from './init/variables/extractors/KeyExtractor';
|
||||
export * from './init/variables/extractors/AssetPathExtractor';
|
||||
|
||||
42
src/init/migration/SingleContainerJsonStorage.ts
Normal file
42
src/init/migration/SingleContainerJsonStorage.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import { JsonResourceStorage } from '../../storage/keyvalue/JsonResourceStorage';
|
||||
import { isContainerIdentifier } from '../../util/PathUtil';
|
||||
import { readableToString } from '../../util/StreamUtil';
|
||||
import { LDP } from '../../util/Vocabularies';
|
||||
|
||||
/**
|
||||
* A variant of a {@link JsonResourceStorage} where the `entries()` call
|
||||
* does not recursively iterate through all containers.
|
||||
* Only the documents that are found in the root container are returned.
|
||||
*
|
||||
* This class was created to support migration where different storages are nested in one main `.internal` container,
|
||||
* and we specifically want to only return entries of one storage.
|
||||
*/
|
||||
export class SingleContainerJsonStorage<T> extends JsonResourceStorage<T> {
|
||||
protected async* getResourceEntries(containerId: ResourceIdentifier): AsyncIterableIterator<[string, T]> {
|
||||
const container = await this.safelyGetResource(containerId);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only need the metadata
|
||||
container.data.destroy();
|
||||
const members = container.metadata.getAll(LDP.terms.contains).map((term): string => term.value);
|
||||
|
||||
for (const path of members) {
|
||||
const documentId = { path };
|
||||
if (isContainerIdentifier(documentId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const document = await this.safelyGetResource(documentId);
|
||||
if (!document) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const json = JSON.parse(await readableToString(document.data));
|
||||
const key = this.identifierToKey(documentId);
|
||||
yield [ key, json ];
|
||||
}
|
||||
}
|
||||
}
|
||||
239
src/init/migration/V6MigrationInitializer.ts
Normal file
239
src/init/migration/V6MigrationInitializer.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { createInterface } from 'readline';
|
||||
import { ACCOUNT_STORAGE_DESCRIPTION } from '../../identity/interaction/account/util/BaseAccountStore';
|
||||
import type { AccountLoginStorage } from '../../identity/interaction/account/util/LoginStorage';
|
||||
import { ACCOUNT_TYPE } from '../../identity/interaction/account/util/LoginStorage';
|
||||
import {
|
||||
CLIENT_CREDENTIALS_STORAGE_DESCRIPTION,
|
||||
CLIENT_CREDENTIALS_STORAGE_TYPE,
|
||||
} from '../../identity/interaction/client-credentials/util/BaseClientCredentialsStore';
|
||||
import {
|
||||
PASSWORD_STORAGE_DESCRIPTION,
|
||||
PASSWORD_STORAGE_TYPE,
|
||||
} from '../../identity/interaction/password/util/BasePasswordStore';
|
||||
import {
|
||||
OWNER_STORAGE_DESCRIPTION,
|
||||
OWNER_STORAGE_TYPE,
|
||||
POD_STORAGE_DESCRIPTION,
|
||||
POD_STORAGE_TYPE,
|
||||
} from '../../identity/interaction/pod/util/BasePodStore';
|
||||
import { WEBID_STORAGE_DESCRIPTION, WEBID_STORAGE_TYPE } from '../../identity/interaction/webid/util/BaseWebIdStore';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||
import { Initializer } from '../Initializer';
|
||||
|
||||
type Account = {
|
||||
webId: string;
|
||||
email: string;
|
||||
password: string;
|
||||
verified: boolean;
|
||||
};
|
||||
|
||||
type Settings = {
|
||||
useIdp: boolean;
|
||||
podBaseUrl?: string;
|
||||
clientCredentials?: string[];
|
||||
};
|
||||
|
||||
type ClientCredentials = {
|
||||
webId: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
const STORAGE_DESCRIPTION = {
|
||||
[ACCOUNT_TYPE]: ACCOUNT_STORAGE_DESCRIPTION,
|
||||
[PASSWORD_STORAGE_TYPE]: PASSWORD_STORAGE_DESCRIPTION,
|
||||
[WEBID_STORAGE_TYPE]: WEBID_STORAGE_DESCRIPTION,
|
||||
[POD_STORAGE_TYPE]: POD_STORAGE_DESCRIPTION,
|
||||
[OWNER_STORAGE_TYPE]: OWNER_STORAGE_DESCRIPTION,
|
||||
[CLIENT_CREDENTIALS_STORAGE_TYPE]: CLIENT_CREDENTIALS_STORAGE_DESCRIPTION,
|
||||
} as const;
|
||||
|
||||
export interface V6MigrationInitializerArgs {
|
||||
/**
|
||||
* The storage in which the version is saved that was stored last time the server was started.
|
||||
*/
|
||||
versionStorage: KeyValueStorage<string, string>;
|
||||
/**
|
||||
* The key necessary to get the version from the `versionStorage`.
|
||||
*/
|
||||
versionKey: string;
|
||||
/**
|
||||
* The storage in which account data of the previous version is stored.
|
||||
*/
|
||||
accountStorage: KeyValueStorage<string, Account | Settings>;
|
||||
/**
|
||||
* The storage in which client credentials are stored from the previous version.
|
||||
*/
|
||||
clientCredentialsStorage: KeyValueStorage<string, ClientCredentials>;
|
||||
/**
|
||||
* The storage in which the forgot password entries of the previous version are stored.
|
||||
* These will all just be removed, not migrated.
|
||||
*/
|
||||
forgotPasswordStorage: KeyValueStorage<string, unknown>;
|
||||
/**
|
||||
* The storage that will contain the account data in the new format.
|
||||
*/
|
||||
newStorage: AccountLoginStorage<any>;
|
||||
/**
|
||||
* If true, no confirmation prompt will be printed to the stdout.
|
||||
*/
|
||||
skipConfirmation?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles migrating account data from v6 to the newer format.
|
||||
* Will only trigger if it is detected that this server was previously started on an older version
|
||||
* and at least one account was found.
|
||||
* Confirmation will be asked to the user through a CLI prompt.
|
||||
* After migration is complete the old data will be removed.
|
||||
*/
|
||||
export class V6MigrationInitializer extends Initializer {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly skipConfirmation: boolean;
|
||||
|
||||
private readonly versionKey: string;
|
||||
private readonly versionStorage: KeyValueStorage<string, string>;
|
||||
|
||||
private readonly accountStorage: KeyValueStorage<string, Account | Settings>;
|
||||
private readonly clientCredentialsStorage: KeyValueStorage<string, ClientCredentials>;
|
||||
private readonly forgotPasswordStorage: KeyValueStorage<string, unknown>;
|
||||
|
||||
private readonly newStorage: AccountLoginStorage<typeof STORAGE_DESCRIPTION>;
|
||||
|
||||
public constructor(args: V6MigrationInitializerArgs) {
|
||||
super();
|
||||
this.skipConfirmation = Boolean(args.skipConfirmation);
|
||||
this.versionKey = args.versionKey;
|
||||
this.versionStorage = args.versionStorage;
|
||||
this.accountStorage = args.accountStorage;
|
||||
this.clientCredentialsStorage = args.clientCredentialsStorage;
|
||||
this.forgotPasswordStorage = args.forgotPasswordStorage;
|
||||
this.newStorage = args.newStorage;
|
||||
}
|
||||
|
||||
public async handle(): Promise<void> {
|
||||
const previousVersion = await this.versionStorage.get(this.versionKey);
|
||||
if (!previousVersion) {
|
||||
// This happens if this is the first time the server is started
|
||||
this.logger.debug('No previous version found');
|
||||
return;
|
||||
}
|
||||
|
||||
const [ prevMajor ] = previousVersion.split('.');
|
||||
if (Number.parseInt(prevMajor, 10) > 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountIterator = this.accountStorage.entries();
|
||||
const next = await accountIterator.next();
|
||||
if (next.done) {
|
||||
this.logger.debug('No account data was found so no migration is necessary.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask the user for confirmation
|
||||
if (!this.skipConfirmation) {
|
||||
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
||||
const answer = await new Promise<string>((resolve): void => {
|
||||
readline.question([
|
||||
'The server is now going to migrate v6 account data to the new storage format internally.',
|
||||
'In case you have not yet done this,',
|
||||
'it is recommended to cancel startup and first backup the existing account data,',
|
||||
'in case something goes wrong.',
|
||||
'When using default configurations with a file backend,',
|
||||
'this data can be found in the ".internal/accounts" folder.',
|
||||
'\n\nDo you want to migrate the data now? [y/N] ',
|
||||
].join(' '), resolve);
|
||||
});
|
||||
readline.close();
|
||||
if (!/^y(?:es)?$/ui.test(answer)) {
|
||||
throw new Error('Stopping server as migration was cancelled.');
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info('Migrating v6 account data to the new format...');
|
||||
|
||||
const webIdAccountMap: Record<string, string> = {};
|
||||
|
||||
// Need to migrate the first entry we already extracted from the iterator above
|
||||
const firstResult = await this.createAccount(next.value[1]);
|
||||
if (firstResult) {
|
||||
// Store link between WebID and account ID for client credentials
|
||||
webIdAccountMap[firstResult.webId] = firstResult.accountId;
|
||||
}
|
||||
|
||||
for await (const [ , account ] of accountIterator) {
|
||||
const result = await this.createAccount(account);
|
||||
if (result) {
|
||||
// Store link between WebID and account ID for client credentials
|
||||
webIdAccountMap[result.webId] = result.accountId;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the existing client credentials tokens
|
||||
for await (const [ label, { webId, secret }] of this.clientCredentialsStorage.entries()) {
|
||||
const accountId = webIdAccountMap[webId];
|
||||
if (!accountId) {
|
||||
this.logger.warn(`Unable to find account for client credentials ${label}. Skipping migration of this token.`);
|
||||
continue;
|
||||
}
|
||||
await this.newStorage.create(CLIENT_CREDENTIALS_STORAGE_TYPE, { webId, label, secret, accountId });
|
||||
}
|
||||
|
||||
// Delete all old entries
|
||||
for await (const [ key ] of this.accountStorage.entries()) {
|
||||
await this.accountStorage.delete(key);
|
||||
}
|
||||
for await (const [ key ] of this.clientCredentialsStorage.entries()) {
|
||||
await this.clientCredentialsStorage.delete(key);
|
||||
}
|
||||
for await (const [ key ] of this.forgotPasswordStorage.entries()) {
|
||||
await this.forgotPasswordStorage.delete(key);
|
||||
}
|
||||
|
||||
this.logger.info('Finished migrating v6 account data.');
|
||||
}
|
||||
|
||||
protected isAccount(data: Account | Settings): data is Account {
|
||||
return Boolean((data as Account).email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new account based on the account data found in the old storage.
|
||||
* Will always create an account and password entry.
|
||||
* In case `useIdp` is true, will create a WebID link entry.
|
||||
* In case there is an associated `podBaseUrl`, will create a pod and owner entry.
|
||||
*/
|
||||
protected async createAccount(account: Account | Settings):
|
||||
Promise<{ accountId: string; webId: string } | undefined> {
|
||||
if (!this.isAccount(account)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { webId, email, password, verified } = account;
|
||||
|
||||
this.logger.debug(`Migrating account ${email} with WebID ${webId}`);
|
||||
|
||||
const settings = await this.accountStorage.get(webId) as Settings | undefined;
|
||||
if (!settings) {
|
||||
this.logger.warn(`Unable to find settings for account ${email}. Skipping migration of this account.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { id: accountId } = await this.newStorage.create(ACCOUNT_TYPE, {});
|
||||
// The `toLowerCase` call is important here to have the expected value
|
||||
await this.newStorage.create(PASSWORD_STORAGE_TYPE,
|
||||
{ email: email.toLowerCase(), password, verified, accountId });
|
||||
if (settings.useIdp) {
|
||||
await this.newStorage.create(WEBID_STORAGE_TYPE, { webId, accountId });
|
||||
}
|
||||
if (settings.podBaseUrl) {
|
||||
const { id: podId } = await this.newStorage.create(POD_STORAGE_TYPE,
|
||||
{ baseUrl: settings.podBaseUrl, accountId });
|
||||
await this.newStorage.create(OWNER_STORAGE_TYPE, { webId, podId, visible: false });
|
||||
}
|
||||
|
||||
return { accountId, webId };
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ const KEY_LENGTH_LIMIT = 255;
|
||||
* All non-404 errors will be re-thrown.
|
||||
*/
|
||||
export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
|
||||
private readonly source: ResourceStore;
|
||||
private readonly container: string;
|
||||
protected readonly source: ResourceStore;
|
||||
protected readonly container: string;
|
||||
|
||||
public constructor(source: ResourceStore, baseUrl: string, container: string) {
|
||||
this.source = source;
|
||||
@@ -79,7 +79,7 @@ export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
|
||||
/**
|
||||
* Recursively iterates through the container to find all documents.
|
||||
*/
|
||||
private async* getResourceEntries(identifier: ResourceIdentifier): AsyncIterableIterator<[string, T]> {
|
||||
protected async* getResourceEntries(identifier: ResourceIdentifier): AsyncIterableIterator<[string, T]> {
|
||||
const representation = await this.safelyGetResource(identifier);
|
||||
if (representation) {
|
||||
if (isContainerIdentifier(identifier)) {
|
||||
@@ -101,7 +101,7 @@ export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
|
||||
* Returns undefined if a 404 error is thrown.
|
||||
* Re-throws the error in all other cases.
|
||||
*/
|
||||
private async safelyGetResource(identifier: ResourceIdentifier): Promise<Representation | undefined> {
|
||||
protected async safelyGetResource(identifier: ResourceIdentifier): Promise<Representation | undefined> {
|
||||
let representation: Representation | undefined;
|
||||
try {
|
||||
const preferences = isContainerIdentifier(identifier) ? {} : { type: { 'application/json': 1 }};
|
||||
@@ -119,7 +119,7 @@ export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
|
||||
/**
|
||||
* Converts a key into an identifier for internal storage.
|
||||
*/
|
||||
private keyToIdentifier(key: string): ResourceIdentifier {
|
||||
protected keyToIdentifier(key: string): ResourceIdentifier {
|
||||
// Parse the key as a file path
|
||||
const parsedPath = parse(key);
|
||||
// Hash long filenames to prevent issues with the underlying storage.
|
||||
@@ -135,7 +135,7 @@ export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
|
||||
/**
|
||||
* Converts an internal identifier to an external key.
|
||||
*/
|
||||
private identifierToKey(identifier: ResourceIdentifier): string {
|
||||
protected identifierToKey(identifier: ResourceIdentifier): string {
|
||||
// Due to the usage of `joinUrl` we don't know for sure if there was a preceding slash,
|
||||
// so we always add one for consistency.
|
||||
// In practice this would only be an issue if a class depends
|
||||
@@ -143,7 +143,7 @@ export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
|
||||
return ensureLeadingSlash(identifier.path.slice(this.container.length));
|
||||
}
|
||||
|
||||
private applyHash(key: string): string {
|
||||
protected applyHash(key: string): string {
|
||||
return createHash('sha256').update(key).digest('hex');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user