diff --git a/packages/solid-crs-id-proxy/config/local-config.json b/packages/solid-crs-id-proxy/config/local-config.json index df30b3c4..8f42c584 100644 --- a/packages/solid-crs-id-proxy/config/local-config.json +++ b/packages/solid-crs-id-proxy/config/local-config.json @@ -390,7 +390,11 @@ "pathToJwks": { "@id": "urn:dgt-id-proxy:variables:jwksFilePath" }, - "webIdPattern": "http://localhost:3007/:customclaim" + "webIdPattern": "http://localhost:3007/:customclaim", + "predicates": [ + [ "http://schema.org/name", [ "https://netwerkdigitaalerfgoed.nl/username" ] ], + [ "http://xmlns.com/foaf/0.1/name", [ "https://netwerkdigitaalerfgoed.nl/username" ] ] + ] }, { "@id": "urn:dgt-id-proxy:default:JwtEncodeResponseHandler" diff --git a/packages/solid-crs-id-proxy/config/proxy-config.json b/packages/solid-crs-id-proxy/config/proxy-config.json index 15cd1118..1d055481 100644 --- a/packages/solid-crs-id-proxy/config/proxy-config.json +++ b/packages/solid-crs-id-proxy/config/proxy-config.json @@ -388,7 +388,11 @@ "pathToJwks": { "@id": "urn:dgt-id-proxy:variables:jwksFilePath" }, - "webIdPattern": "https://webid.netwerkdigitaalerfgoed.nl/:customclaim" + "webIdPattern": "https://webid.netwerkdigitaalerfgoed.nl/:customclaim", + "predicates": [ + [ "http://schema.org/name", [ "https://netwerkdigitaalerfgoed.nl/username" ] ], + [ "http://xmlns.com/foaf/0.1/name", [ "https://netwerkdigitaalerfgoed.nl/username" ] ] + ] }, { "@id": "urn:dgt-id-proxy:default:JwtEncodeResponseHandler" diff --git a/packages/solid-crs-manage/.env.development b/packages/solid-crs-manage/.env.development index 93fed5ad..58c51ab3 100644 --- a/packages/solid-crs-manage/.env.development +++ b/packages/solid-crs-manage/.env.development @@ -3,3 +3,4 @@ VITE_TERM_ENDPOINT=https://termennetwerk-api.netwerkdigitaalerfgoed.nl/graphql VITE_ID_PROXY_URI=http://localhost:3006/ VITE_WEBID_URI=http://localhost:3007/ VITE_PRESENTATION_URI=http://localhost:3005/ +VITE_PODS_URI=http://localhost:3000/ diff --git a/packages/solid-crs-manage/.env.production b/packages/solid-crs-manage/.env.production index ec88d5e0..585224bc 100644 --- a/packages/solid-crs-manage/.env.production +++ b/packages/solid-crs-manage/.env.production @@ -3,3 +3,4 @@ VITE_TERM_ENDPOINT=https://termennetwerk-api.netwerkdigitaalerfgoed.nl/graphql VITE_ID_PROXY_URI=https://auth.netwerkdigitaalerfgoed.nl/ VITE_WEBID_URI=https://webid.netwerkdigitaalerfgoed.nl/ VITE_PRESENTATION_URI=https://solid-crs-presentatie.netwerkdigitaalerfgoed.nl/ +VITE_PODS_URI=https://pods.netwerkdigitaalerfgoed.nl/ diff --git a/packages/solid-crs-manage/lib/app-root.component.ts b/packages/solid-crs-manage/lib/app-root.component.ts index f7371977..aae495c9 100644 --- a/packages/solid-crs-manage/lib/app-root.component.ts +++ b/packages/solid-crs-manage/lib/app-root.component.ts @@ -8,8 +8,8 @@ import { RxLitElement } from 'rx-lit'; import { Theme, Logout, Plus, Cross, Search } from '@netwerk-digitaal-erfgoed/solid-crs-theme'; import { unsafeSVG } from 'lit-html/directives/unsafe-svg'; import { define, hydrate } from '@digita-ai/dgt-components'; -import { Client, Session, SolidSDKService } from '@digita-ai/inrupt-solid-service'; -import { AppActors, AppAuthenticateStates, AppContext, AppDataStates, AppFeatureStates, appMachine, AppRootStates } from './app.machine'; +import { Session, SolidSDKService } from '@digita-ai/inrupt-solid-service'; +import { AppActors, AppAuthenticateStates, AppContext, AppFeatureStates, appMachine, AppRootStates } from './app.machine'; import { AppEvents, ClickedCreateCollectionEvent, DismissAlertEvent, LoggedInEvent } from './app.events'; import { CollectionEvents } from './features/collection/collection.events'; import { SearchEvent, SearchUpdatedEvent } from './features/search/search.events'; @@ -175,7 +175,8 @@ export class AppRootComponent extends RxLitElement { )).withContext({ alerts: [], }), { devTools: process.env.MODE === 'DEV' }, - ); + // eslint-disable-next-line no-console + ).onTransition((state) => { if (process.env.MODE === 'DEV') console.log(state.value); }); this.subscribe('state', from(this.actor)); @@ -260,11 +261,9 @@ export class AppRootComponent extends RxLitElement { */ render(): TemplateResult { - const showLoading = this.state?.matches({ [AppRootStates.DATA]: AppDataStates.CREATING }) - || this.state?.matches({ [AppRootStates.DATA]: AppDataStates.REFRESHING }); + const showLoading = this.state?.hasTag('loading'); - const hideSidebar = this.state?.matches({ [AppRootStates.DATA]: AppDataStates.DETERMINING_POD_TYPE }) - || this.state?.matches({ [AppRootStates.DATA]: AppDataStates.CHECKING_TYPE_REGISTRATIONS }) + const hideSidebar = this.state?.hasTag('setup') || !this.state?.matches({ [AppRootStates.AUTHENTICATE]: AppAuthenticateStates.AUTHENTICATED }); return html` @@ -307,13 +306,14 @@ export class AppRootComponent extends RxLitElement { ` : '' } - ${ !this.state?.matches({ [AppRootStates.AUTHENTICATE]: AppAuthenticateStates.AUTHENTICATED }) + ${ this.state?.matches({ [AppRootStates.AUTHENTICATE]: AppAuthenticateStates.UNAUTHENTICATED }) ? html``: ''} - ${ this.state?.matches({ [AppRootStates.DATA]: AppDataStates.DETERMINING_POD_TYPE }) ? html`` : html` + ${ this.state?.hasTag('setup') ? html`` : html` ${ this.state?.matches({ [AppRootStates.AUTHENTICATE]: AppAuthenticateStates.AUTHENTICATED, [AppRootStates.FEATURE]: AppFeatureStates.COLLECTION }) ? html`` : '' } ${ this.state?.matches({ [AppRootStates.AUTHENTICATE]: AppAuthenticateStates.AUTHENTICATED, [AppRootStates.FEATURE]: AppFeatureStates.SEARCH }) ? html`` : '' } ${ this.state?.matches({ [AppRootStates.AUTHENTICATE]: AppAuthenticateStates.AUTHENTICATED, [AppRootStates.FEATURE]: AppFeatureStates.OBJECT }) ? html`` : '' } diff --git a/packages/solid-crs-manage/lib/app.events.ts b/packages/solid-crs-manage/lib/app.events.ts index 49b9c577..31a3a667 100644 --- a/packages/solid-crs-manage/lib/app.events.ts +++ b/packages/solid-crs-manage/lib/app.events.ts @@ -22,6 +22,7 @@ export enum AppEvents { CLICKED_ADMINISTRATOR_TYPE = '[AppEvent: Clicked Admin Pod Type]', CLICKED_INSTITUTION_TYPE = '[AppEvent: Clicked Institution Pod Type]', SET_PROFILE = '[AppEvent: Set Profile]', + CLICKED_CREATE_POD = '[AppEvent: Clicked Create Pod]', } /** @@ -70,7 +71,7 @@ export class LoggedOutEvent implements EventObject { /** * An event which is dispatched when an error occurs. */ -export class LoggingOutEvent implements EventObject { +export class ClickedLogoutEvent implements EventObject { public type: AppEvents.CLICKED_LOGOUT = AppEvents.CLICKED_LOGOUT; @@ -122,13 +123,22 @@ export class SetProfileEvent implements EventObject { } +/** + * An event which is dispatched when an error occurs. + */ +export class ClickedCreatePodEvent implements EventObject { + + public type: AppEvents.CLICKED_CREATE_POD = AppEvents.CLICKED_CREATE_POD; + +} + /** * Union type of app events. */ export type AppEvent = | RouterEvent | LoggedInEvent - | LoggingOutEvent + | ClickedLogoutEvent | LoggedOutEvent | ErrorEvent | DismissAlertEvent @@ -142,7 +152,8 @@ export type AppEvent = | ClickedDeleteObjectEvent | ClickedInstitutionTypeEvent | ClickedAdministratorTypeEvent - | SetProfileEvent; + | SetProfileEvent + | ClickedCreatePodEvent; /** * Actions for the alerts component. diff --git a/packages/solid-crs-manage/lib/app.machine.spec.ts b/packages/solid-crs-manage/lib/app.machine.spec.ts index cc66bf12..0f15abb9 100644 --- a/packages/solid-crs-manage/lib/app.machine.spec.ts +++ b/packages/solid-crs-manage/lib/app.machine.spec.ts @@ -12,6 +12,7 @@ const solidService = { uri: 'https://example.com/profile', })), logout: jest.fn(async() => undefined), + getStorages: jest.fn(async () => [ 'https://storage.uri/' ]), } as any; describe('AppMachine', () => { diff --git a/packages/solid-crs-manage/lib/app.machine.ts b/packages/solid-crs-manage/lib/app.machine.ts index a6435457..b6b07a98 100644 --- a/packages/solid-crs-manage/lib/app.machine.ts +++ b/packages/solid-crs-manage/lib/app.machine.ts @@ -2,14 +2,15 @@ import { Alert, FormActors, formMachine, FormValidatorResult, State } from '@net import { Collection, CollectionObjectStore, CollectionObject, CollectionStore, SolidProfile, SolidSession, Route, routerStateConfig, NavigatedEvent, RouterStates, createRoute, activeRoute, routerEventsConfig, RouterEvents, updateHistory } from '@netwerk-digitaal-erfgoed/solid-crs-core'; import { createMachine } from 'xstate'; import { assign, forwardTo, log, send } from 'xstate/lib/actions'; -import { SolidService } from '@digita-ai/inrupt-solid-service'; -import { addAlert, AddAlertEvent, addCollection, AppEvent, AppEvents, dismissAlert, LoggedOutEvent, LoggingOutEvent, removeSession, setCollections, setProfile, SetProfileEvent, setSession } from './app.events'; +import { SolidSDKService } from '@digita-ai/inrupt-solid-service'; +import { addAlert, AddAlertEvent, addCollection, AppEvent, AppEvents, dismissAlert, LoggedOutEvent, ClickedLogoutEvent, removeSession, setCollections, setProfile, SetProfileEvent, setSession } from './app.events'; import { collectionMachine } from './features/collection/collection.machine'; import { CollectionEvents, SelectedCollectionEvent } from './features/collection/collection.events'; import { searchMachine } from './features/search/search.machine'; import { SearchEvents, SearchUpdatedEvent } from './features/search/search.events'; import { objectMachine } from './features/object/object.machine'; import { ObjectEvents } from './features/object/object.events'; +import { createPod } from './app.services'; /** * The root context of the application. @@ -92,6 +93,9 @@ export enum AppDataStates { CREATING = '[AppDataStates: Creating]', CHECKING_TYPE_REGISTRATIONS = '[AppDataStates: Checking Type Registrations]', DETERMINING_POD_TYPE = '[AppDataStates: Determining Pod Type]', + CHECKING_STORAGE= '[AppDataStates: Checking Storage]', + AWAITING_POD_CREATION= '[AppDataStates: Awaiting Pod Creation]', + CREATING_POD= '[AppDataStates: Creating Pod]', } /** @@ -122,7 +126,7 @@ export const routes: Route[] = [ * The application root machine and its configuration. */ export const appMachine = ( - solid: SolidService, + solid: SolidSDKService, collectionStore: CollectionStore, objectStore: CollectionObjectStore, collectionTemplate: Collection, @@ -363,13 +367,16 @@ export const appMachine = ( */ src: () => solid.logout(), onDone: { - actions: send(new LoggedOutEvent()), + actions: [ + send(new LoggedOutEvent()), + () => window.localStorage.removeItem('solidClientAuthn:currentSession'), + ], }, }, on: { [AppEvents.LOGGED_OUT]: { target: AppAuthenticateStates.UNAUTHENTICATED, - actions: () => location.reload(), + // actions: () => location.reload(), }, }, }, @@ -388,7 +395,7 @@ export const appMachine = ( }, }, /** - * Determines if the current user is creating a collection. + * Handles pod config and collection creation. */ [AppRootStates.DATA]: { initial: AppDataStates.IDLE, @@ -399,17 +406,68 @@ export const appMachine = ( [AppDataStates.IDLE]: { on: { [AppEvents.CLICKED_CREATE_COLLECTION]: AppDataStates.CREATING, - [AppEvents.SET_PROFILE]: AppDataStates.CHECKING_TYPE_REGISTRATIONS, + [AppEvents.SET_PROFILE]: AppDataStates.CHECKING_STORAGE, [CollectionEvents.CLICKED_DELETE]: AppDataStates.REFRESHING, [ObjectEvents.CLICKED_DELETE]: AppDataStates.REFRESHING, [CollectionEvents.SAVED_COLLECTION]: AppDataStates.REFRESHING, }, }, + /** + * Check whether a storage triple is present in the WebID. + */ + [AppDataStates.CHECKING_STORAGE]: { + tags: [ 'setup', 'loading' ], + invoke: { + src: (context) => solid.getStorages(context.session.webId), + onDone: [ + { + target: AppDataStates.AWAITING_POD_CREATION, + cond: (c: AppContext, event) => event?.data && event.data.length === 0, + }, + { + target: AppDataStates.CHECKING_TYPE_REGISTRATIONS, + }, + ], + onError: send((c, event) => event), + }, + + }, + /** + * No storage triple present in the WebID, waiting for user input + */ + [AppDataStates.AWAITING_POD_CREATION]: { + tags: [ 'setup' ], + on: { + [AppEvents.CLICKED_LOGOUT]: { + target: AppDataStates.IDLE, + actions: send((c, event) => event), + }, + [AppEvents.CLICKED_CREATE_POD]: { + target: AppDataStates.CREATING_POD, + }, + }, + }, + /** + * Creates a Solid pod at pods.netwerkdigitaalerfgoed.nl. + */ + [AppDataStates.CREATING_POD]: { + tags: [ 'setup', 'loading' ], + invoke: { + src: () => createPod(solid), + onDone: { + target: AppDataStates.CHECKING_TYPE_REGISTRATIONS, + }, + onError: { + actions: send((c, event) => event), + }, + }, + }, /** * Checks existance of DataCatalog type registration * When none was found, further set-up is needed (DETERMINING_POD_TYPE) */ [AppDataStates.CHECKING_TYPE_REGISTRATIONS]: { + tags: [ 'setup', 'loading' ], invoke: { src: (context) => collectionStore.getInstanceForClass(context.profile.uri, 'http://schema.org/DataCatalog'), onDone: [ @@ -424,7 +482,7 @@ export const appMachine = ( onError: { actions: [ send(new AddAlertEvent({ message: 'authenticate.error.no-valid-type-registration', type: 'warning' })), - send(new LoggingOutEvent()), + send(new ClickedLogoutEvent()), ], }, }, @@ -435,6 +493,7 @@ export const appMachine = ( * or that it is an administrator's pod, accessing an institution's pod (pod type: administrator) */ [AppDataStates.DETERMINING_POD_TYPE]: { + tags: [ 'setup' ], on: { [AppEvents.CLICKED_ADMINISTRATOR_TYPE]: [ { @@ -442,7 +501,7 @@ export const appMachine = ( target: AppDataStates.IDLE, actions: [ send(new AddAlertEvent({ message: 'authenticate.error.no-valid-type-registration', type: 'warning' })), - send(new LoggingOutEvent()), + send(new ClickedLogoutEvent()), ], }, ], @@ -461,6 +520,7 @@ export const appMachine = ( */ [AppDataStates.REFRESHING]: { id: AppDataStates.REFRESHING, + tags: [ 'loading' ], invoke: { /** * Get all collections from store. @@ -488,6 +548,7 @@ export const appMachine = ( * Creating a new collection. */ [AppDataStates.CREATING]: { + tags: [ 'loading' ], invoke: { /** * Save collection to the store. diff --git a/packages/solid-crs-manage/lib/app.services.spec.ts b/packages/solid-crs-manage/lib/app.services.spec.ts new file mode 100644 index 00000000..c3f27d89 --- /dev/null +++ b/packages/solid-crs-manage/lib/app.services.spec.ts @@ -0,0 +1,233 @@ +import * as sdk from '@digita-ai/inrupt-solid-client'; +import { SolidSDKService } from '@digita-ai/inrupt-solid-service'; +import fetchMock from 'jest-fetch-mock'; +import { createPod } from './app.services'; + +describe('Authorization Services', () => { + + let solidService: SolidSDKService; + + beforeEach(() => { + + solidService = { + getStorages: jest.fn(async () => [ 'https://link.to/storage' ]), + getIssuers: jest.fn(async () => + [ { icon: '', description: 'mock issuer', uri: process.env.VITE_ID_PROXY } ]), + getSession: jest.fn(async () => ({ webId: 'https://web.id/profile' })), + getDefaultSession: jest.fn(() => ({ + fetch, + info: { + sessionId: 'test-id', + webId: 'https://web.id/', + }, + })), + } as any; + + }); + + describe('createPod', () => { + + it('should error when session does not contain webId', async () => { + + (solidService.getDefaultSession as unknown) = jest.fn(() => undefined); + + await expect(createPod(solidService)).rejects.toThrow('Not logged in'); + + }); + + it('should error when first pod request does not return 400', async () => { + + fetchMock.mockOnce(async() => 'success'); + + await expect(createPod(solidService)).rejects.toThrow('Server returned error (first request)'); + + }); + + it('should error when no profile thing could be found when writing oidc registration', async () => { + + fetchMock.mockOnce(async () => ({ + body: JSON.stringify({ + details: { + quad: ' "token"', + }, + }), + init: { status: 400 }, + })); + + (sdk.getSolidDataset as unknown) = jest.fn(async () => true); + (sdk.getThing as unknown) = jest.fn(() => null); + + await expect(createPod(solidService)).rejects.toThrow('Could not retrieve profile for'); + + }); + + it('should error when second pod request was unsuccessful', async () => { + + fetchMock.mockOnce(async () => ({ + body: JSON.stringify({ + details: { + quad: ' "token"', + }, + }), + init: { status: 400 }, + })); + + fetchMock.mockOnce(async () => ({ + body: 'internal server error', + init: { status: 500 }, + })); + + (sdk.getSolidDataset as unknown) = jest.fn(async () => true); + (sdk.getThing as unknown) = jest.fn(() => true); + (sdk.addStringNoLocale as unknown) = jest.fn(() => true); + (sdk.setThing as unknown) = jest.fn(() => true); + (sdk.saveSolidDatasetAt as unknown) = jest.fn(async () => true); + + await expect(createPod(solidService)).rejects.toThrow('Server returned error (second request)'); + + }); + + it('should error when no profile thing could be found when writing storage', async () => { + + fetchMock.mockOnce(async () => ({ + body: JSON.stringify({ + details: { + quad: ' "token"', + }, + }), + init: { status: 400 }, + })); + + fetchMock.mockOnce(async() => '{"status": "success"}'); + + let checkingStorage = false; + + (sdk.getSolidDataset as unknown) = jest.fn(async () => true); + + (sdk.getThing as unknown) = jest.fn(() => { + + if (!checkingStorage) { + + // pass first time + checkingStorage = true; + + return true; + + } else { + + // second time, return null + return null; + + } + + }); + + (sdk.addStringNoLocale as unknown) = jest.fn(() => true); + (sdk.setThing as unknown) = jest.fn(() => true); + (sdk.saveSolidDatasetAt as unknown) = jest.fn(async () => true); + + await expect(createPod(solidService)).rejects.toThrow('Could not retrieve profile for'); + + }); + + it('should return the webId when succesful', async () => { + + fetchMock.mockOnce(async () => ({ + body: JSON.stringify({ + details: { + quad: ' "token"', + }, + }), + init: { status: 400 }, + })); + + fetchMock.mockOnce(async() => '{"status": "success"}'); + + (sdk.getSolidDataset as unknown) = jest.fn(async () => true); + (sdk.getThing as unknown) = jest.fn(() => true); + (sdk.addStringNoLocale as unknown) = jest.fn(() => true); + (sdk.getUrl as unknown) = jest.fn(() => true); + (sdk.addUrl as unknown) = jest.fn(() => true); + (sdk.setThing as unknown) = jest.fn(() => true); + (sdk.saveSolidDatasetAt as unknown) = jest.fn(async () => true); + (sdk.overwriteFile as unknown) = jest.fn(async () => true); + + await expect(createPod(solidService)).resolves.toEqual(solidService.getDefaultSession().info.webId); + + }); + + it('should create privateTypeIndex file when triple missing from pod', async () => { + + fetchMock.mockOnce(async () => ({ + body: JSON.stringify({ + details: { + quad: ' "token"', + }, + }), + init: { status: 400 }, + })); + + fetchMock.mockOnce(async() => '{"status": "success"}'); + + fetchMock.mockOnce(async () => ({ + init: { status: 404 }, + })); + + (sdk.getSolidDataset as unknown) = jest.fn(async () => true); + (sdk.getThing as unknown) = jest.fn(() => true); + (sdk.addStringNoLocale as unknown) = jest.fn(() => true); + (sdk.addUrl as unknown) = jest.fn(() => true); + + (sdk.getUrl as unknown) = jest.fn((t, predicate) => + predicate !== 'http://www.w3.org/ns/solid/terms#privateTypeIndex'); + + (sdk.setThing as unknown) = jest.fn(() => true); + (sdk.saveSolidDatasetAt as unknown) = jest.fn(async () => true); + (sdk.overwriteFile as unknown) = jest.fn(async () => true); + + await expect(createPod(solidService)).resolves.toEqual(solidService.getDefaultSession().info.webId); + expect(sdk.overwriteFile).toHaveBeenCalledTimes(1); + + }); + + it('should create publicTypeIndex file when triple missing from pod', async () => { + + fetchMock.mockOnce(async () => ({ + body: JSON.stringify({ + details: { + quad: ' "token"', + }, + }), + init: { status: 400 }, + })); + + fetchMock.mockOnce(async() => '{"status": "success"}'); + + fetchMock.mockOnce(async () => ({ + init: { status: 404 }, + })); + + fetchMock.mockOnce(async () => ({ + init: { status: 404 }, + })); + + (sdk.getSolidDataset as unknown) = jest.fn(async () => true); + (sdk.getThing as unknown) = jest.fn(() => true); + (sdk.addStringNoLocale as unknown) = jest.fn(() => true); + (sdk.addUrl as unknown) = jest.fn(() => true); + + (sdk.getUrl as unknown) = jest.fn((t, predicate) => + predicate !== 'http://www.w3.org/ns/solid/terms#publicTypeIndex'); + + (sdk.setThing as unknown) = jest.fn(() => true); + (sdk.saveSolidDatasetAt as unknown) = jest.fn(async () => true); + (sdk.overwriteFile as unknown) = jest.fn(async () => true); + + await expect(createPod(solidService)).resolves.toEqual(solidService.getDefaultSession().info.webId); + expect(sdk.overwriteFile).toHaveBeenCalledTimes(1); + + }); + + }); + +}); diff --git a/packages/solid-crs-manage/lib/app.services.ts b/packages/solid-crs-manage/lib/app.services.ts new file mode 100644 index 00000000..7f3608b1 --- /dev/null +++ b/packages/solid-crs-manage/lib/app.services.ts @@ -0,0 +1,149 @@ +import { SolidSDKService } from '@digita-ai/inrupt-solid-service'; +import { v5 } from 'uuid'; +import { addStringNoLocale, addUrl, getSolidDataset, getThing, getUrl, overwriteFile, saveSolidDatasetAt, setThing } from '@digita-ai/inrupt-solid-client'; + +/** + * Creates a Solid pod at https://pods.netwerkdigitaalerfgoed.nl/ + * + * @param context The authorization context + * @param event The authorization event type + * @returns The WebID of the pod + */ +export const createPod = async (solidService: SolidSDKService): Promise => { + + const session = solidService.getDefaultSession(); + + if (!session?.info?.webId) { + + throw Error('Not logged in'); + + } + + const createPodRequest = async (webId: string) => await fetch( + `${process.env.VITE_PODS_URI}idp/register/`, + { + method: 'post', + headers: { + 'content-type': 'application/json', + accept: 'application/json', + }, + body: JSON.stringify({ + webId, + createPod: 'on', + podName: v5(webId, 'a2e08b56-67b9-49b9-894f-696052dbef9a'), + email: `${v5(webId, 'a2e08b56-67b9-49b9-894f-696052dbef9a')}@digita.ai`, + password: 'a', + confirmPassword: 'a', + }), + } + ); + + // retrieve quad + const oidcIssuerRegistrationToken = await createPodRequest(session.info.webId) + .then(async (res) => { + + if (res.status === 400) { + + return (await res.json()).details.quad.split('')[1].replace(/[^\w\d-]*/ig, ''); + + } else { + + throw new Error('Server returned error (first request)'); + + } + + }); + + // add missing oidcRegistration quad + let dataset = await getSolidDataset(session.info.webId, { fetch: session.fetch }); + let profileThing = getThing(dataset, session.info.webId); + + if (!profileThing) { + + throw Error(`Could not retrieve profile for ${session.info.webId}`); + + } + + profileThing = addStringNoLocale(profileThing, 'http://www.w3.org/ns/solid/terms#oidcIssuerRegistrationToken', oidcIssuerRegistrationToken); + dataset = setThing(dataset, profileThing); + await saveSolidDatasetAt(session.info.webId, dataset, { fetch: session.fetch }); + + // create pod, return baseUrl + const podBaseUrl = await createPodRequest(session.info.webId).then(async (res) => { + + if (!res.ok) { + + throw Error('Server returned error (second request)'); + + } else { + + return (await res.json()).podBaseUrl; + + } + + }); + + // add storage triple + default type indexes to pod + dataset = await getSolidDataset(session.info.webId, { fetch: session.fetch }); + profileThing = getThing(dataset, session.info.webId); + + if (!profileThing) { + + throw Error(`Could not retrieve profile for ${session.info.webId}`); + + } + + const privateTypeIndex = getUrl(profileThing, 'http://www.w3.org/ns/solid/terms#privateTypeIndex'); + + if (!privateTypeIndex) { + + await session.fetch(`${podBaseUrl}settings/privateTypeIndex`, { method: 'head' }).then(async (res) => { + + if (res.status === 404) { + + await overwriteFile(`${podBaseUrl}settings/privateTypeIndex`, new Blob([ + `@prefix solid: . +<> + a solid:TypeIndex ; + a solid:UnlistedDocument.`, + ], { type: 'text/turtle' }), { fetch: session.fetch }); + + } + + }); + + profileThing = addUrl(profileThing, 'http://www.w3.org/ns/solid/terms#privateTypeIndex', `${podBaseUrl}settings/privateTypeIndex`); + + } + + const publicTypeIndex = getUrl(profileThing, 'http://www.w3.org/ns/solid/terms#publicTypeIndex'); + + if (!publicTypeIndex) { + + await session.fetch(`${podBaseUrl}settings/publicTypeIndex`, { method: 'head' }).then(async (res) => { + + if (res.status === 404) { + + await overwriteFile(`${podBaseUrl}settings/publicTypeIndex`, new Blob([ + `@prefix solid: . +<> + a solid:TypeIndex ; + a solid:ListedDocument.`, + ], { type: 'text/turtle' }), { fetch: session.fetch }); + + } + + }); + + profileThing = addUrl(profileThing, 'http://www.w3.org/ns/solid/terms#publicTypeIndex', `${podBaseUrl}settings/publicTypeIndex`); + + } + + profileThing = addUrl(profileThing, 'http://www.w3.org/ns/pim/space#storage', podBaseUrl); + // todo add default type index files + dataset = setThing(dataset, profileThing); + await saveSolidDatasetAt(session.info.webId, dataset, { fetch: session.fetch }); + + return session.info.webId; + +}; diff --git a/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.spec.ts b/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.spec.ts index 92e1d587..f5a8ddc7 100644 --- a/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.spec.ts +++ b/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.spec.ts @@ -1,18 +1,70 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { define, hydrate } from '@digita-ai/dgt-components'; +import { Alert, define, hydrate } from '@digita-ai/dgt-components'; +import { SolidSDKService } from '@digita-ai/inrupt-solid-service'; +import { ArgumentError, Collection, CollectionObject, CollectionObjectMemoryStore, CollectionSolidStore } from '@netwerk-digitaal-erfgoed/solid-crs-core'; +import { interpret, Interpreter } from 'xstate'; +import { DismissAlertEvent } from '../../app.events'; +import { AppContext, appMachine } from '../../app.machine'; import { AuthenticateRootComponent } from './authenticate-root.component'; describe('AuthenticateRootComponent', () => { let component: AuthenticateRootComponent; + let machine: Interpreter; + let solidService: SolidSDKService; + let collectionStore: CollectionSolidStore; + + const collection1: Collection = { + uri: 'collection-uri-3', + name: 'Collection 3', + description: 'This is collection 3', + objectsUri: 'test-uri', + distribution: 'test-uri', + }; + + const object1: CollectionObject = { + uri: 'object-uri-1', + name: 'Object 1', + description: 'This is object 1', + image: null, + subject: null, + type: null, + updated: '0', + collection: 'collection-uri-1', + }; beforeEach(() => { - const solidService = ({} as any); + solidService = { + getStorages: jest.fn(async () => [ 'https://storage.uri/' ]), + } as any; + + collectionStore = { + getInstanceForClass: jest.fn(async () => undefined), + } as any; + + machine = interpret(appMachine( + solidService, + collectionStore, + new CollectionObjectMemoryStore([ + object1, + ]), + collection1, + object1 + ) + .withContext({ + alerts: [ { type: 'warning', message: 'test' } ], + session: { webId: 'lorem' }, + profile: { + name: 'Lea Peeters', + uri: 'https://web.id/', + }, + })); const tag = 'authenticate-component'; define(tag, hydrate(AuthenticateRootComponent)(solidService)); component = window.document.createElement(tag) as AuthenticateRootComponent; + component.appActor = machine; }); @@ -28,4 +80,64 @@ describe('AuthenticateRootComponent', () => { }); + describe('firstUpdated', () => { + + it('should subscribe to alerts', async () => { + + component.appActor.start(); + component.subscribe = jest.fn(); + + const changed = new Map(); + changed.set('appActor', machine); + component.firstUpdated(changed); + + window.document.body.appendChild(component); + await component.updateComplete; + + expect(component.subscribe).toHaveBeenCalledWith('alerts', expect.anything()); + + }); + + }); + + describe('handleDismiss', () => { + + const alert: Alert = { message: 'foo', type: 'success' }; + + it('should throw error when event is null', async () => { + + window.document.body.appendChild(component); + await component.updateComplete; + + expect(() => component.handleDismiss(null)).toThrow(ArgumentError); + + }); + + it('should throw error when actor is null', async () => { + + component.appActor = null; + + window.document.body.appendChild(component); + await component.updateComplete; + + expect(() => component.handleDismiss({ detail: alert } as CustomEvent)).toThrow(ArgumentError); + + }); + + it('should send dismiss alert event to parent', async () => { + + machine.start(); + + window.document.body.appendChild(component); + await component.updateComplete; + + machine.send = jest.fn(); + + component.handleDismiss({ detail: alert } as CustomEvent); + expect(machine.send).toHaveBeenCalledWith(new DismissAlertEvent(alert)); + + }); + + }); + }); diff --git a/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.ts b/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.ts index 22302ea4..4b3ba51b 100644 --- a/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.ts +++ b/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.ts @@ -1,11 +1,16 @@ -import { html, property, internalProperty, unsafeCSS, css, TemplateResult, CSSResult } from 'lit-element'; +import { html, property, internalProperty, unsafeCSS, css, TemplateResult, CSSResult, PropertyValues } from 'lit-element'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; -import { Logger, Translator, validateWebId } from '@netwerk-digitaal-erfgoed/solid-crs-core'; +import { ArgumentError, validateWebId, Logger, Translator } from '@netwerk-digitaal-erfgoed/solid-crs-core'; import { RxLitElement } from 'rx-lit'; import { unsafeSVG } from 'lit-html/directives/unsafe-svg'; import { Login, Logo, Theme } from '@netwerk-digitaal-erfgoed/solid-crs-theme'; import { AuthenticateComponent, define, hydrate } from '@digita-ai/dgt-components'; -import { Client, Issuer, Session, SolidService } from '@digita-ai/inrupt-solid-service'; +import { Issuer, Session, SolidService } from '@digita-ai/inrupt-solid-service'; +import { Interpreter } from 'xstate'; +import { Alert } from '@netwerk-digitaal-erfgoed/solid-crs-components'; +import { from, map } from 'rxjs'; +import { AppContext } from '../../app.machine'; +import { DismissAlertEvent } from '../../app.events'; /** * The root page of the authenticate feature. @@ -24,6 +29,18 @@ export class AuthenticateRootComponent extends RxLitElement { @property({ type: Object }) translator: Translator; + /** + * The component's translator. + */ + @property({ type: Object }) + appActor: Interpreter; + + /** + * The component's alerts. + */ + @internalProperty() + alerts: Alert[]; + /** * List of predefined issuers. */ @@ -42,6 +59,22 @@ export class AuthenticateRootComponent extends RxLitElement { } + /** + * Hook called on first update after connection to the DOM. + */ + firstUpdated(changed: PropertyValues): void { + + if(changed && changed.has('appActor') && !!changed.get('appActor')){ + + this.subscribe('alerts', from(this.appActor) + .pipe(map((state) => state.context?.alerts))); + + } + + super.firstUpdated(changed); + + } + /** * Dsipatches an authenticated event to parent element when the user is authenticated. * @@ -53,6 +86,29 @@ export class AuthenticateRootComponent extends RxLitElement { }; + /** + * Handles a dismiss event by sending a dismiss alert event to its parent. + * + * @param event Dismiss event dispatched by an alert componet. + */ + handleDismiss(event: CustomEvent): void { + + if (!event || !event.detail) { + + throw new ArgumentError('Argument event || event.detail should be set.', event); + + } + + if (!this.appActor) { + + throw new ArgumentError('Argument this.appActor should be set.', this.appActor); + + } + + this.appActor.send(new DismissAlertEvent(event.detail)); + + } + /** * Renders the component as HTML. * @@ -60,22 +116,35 @@ export class AuthenticateRootComponent extends RxLitElement { */ render(): TemplateResult { + const alerts = this.alerts?.map((alert) => html` + + `); + return html`
${ unsafeSVG(Logo) }

${this.translator?.translate('authenticate.pages.login.title')}

+ +
+ ${ alerts } +
+ hideCreateNewWebId + @authenticated="${this.onAuthenticated}" + .predefinedIssuers="${this.issuers}" + .textSeparator="${ this.translator?.translate('authenticate.pages.login.separator') }" + .textWebIdLabel="${ this.translator?.translate('authenticate.pages.login.webid-label') }" + .textWebIdPlaceholder="${ this.translator?.translate('authenticate.pages.login.webid-placeholder') }" + .textButton="${Login}" + .translator="${this.translator}" + > +

${unsafeHTML(this.translator?.translate('authenticate.pages.login.create-webid'))}

@@ -96,14 +165,42 @@ export class AuthenticateRootComponent extends RxLitElement { flex-direction: column; justify-content: center; align-items: center; - gap: 80px; background-color: var(--colors-primary-dark); + font-size: var(--font-size-small); } - authenticate-component { + :host > *:not(style) { width: 400px; max-width: 400px; min-width: 400px; } + .title-container { + height: 50px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: var(--gap-large); + color: var(--colors-foreground-inverse); + margin-bottom: var(--gap-large); + } + .title-container svg { + max-height: 50px; + height: 50px; + max-width: 50px; + width: 50px; + fill: var(--colors-foreground-inverse); + stroke: none !important; + } + .title-container h1 { + font-size: var(--font-size-header-normal); + font-weight: normal; + } + .alert-container { + margin-top: var(--gap-large); + } + authenticate-component { + margin: var(--gap-large) 0; + } authenticate-component::part(provider) { border: none; background-color: var(--colors-primary-light); @@ -113,7 +210,6 @@ export class AuthenticateRootComponent extends RxLitElement { padding: 0 0 0 6.5rem; margin: 0; height: 40px; - font-size: var(--font-size-small); } authenticate-component::part(webid-label) { @@ -137,7 +233,6 @@ export class AuthenticateRootComponent extends RxLitElement { --colors-foreground-light: white; width: 75%; } - authenticate-component::part(webid-button) { border: none; background-color: var(--colors-primary-light); @@ -151,55 +246,32 @@ export class AuthenticateRootComponent extends RxLitElement { height: var(--gap-large); width: 60px; } - authenticate-component::part(alert) { height: var(--gap-small); font-size: var(--font-size-small); } - authenticate-component::part(loading) { --colors-foreground-normal: var(--colors-foreground-light); } - .title-container { - height: 50px; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - gap: var(--gap-large); - color: var(--colors-foreground-inverse); - } - - .title-container svg { - max-height: 50px; - height: 50px; - max-width: 50px; - width: 50px; - fill: var(--colors-foreground-inverse); - stroke: none !important; - } - - .title-container h1 { - font-size: var(--font-size-header-normal); - font-weight: normal; + authenticate-component::part(provider-logo) { + display: none; } - nde-form-element label { color: white; } - + .webid-container { + margin-top: var(--gap-large); + } .webid-container p { text-align: center; color: var(--colors-foreground-light); font-size: var(--font-size-small); margin: 0; } - .webid-container p a { color: var(--colors-foreground-light); } - svg { stroke: var(--colors-foreground-light) !important; } */ diff --git a/packages/solid-crs-manage/lib/features/authenticate/authenticate-setup.component.spec.ts b/packages/solid-crs-manage/lib/features/authenticate/authenticate-setup.component.spec.ts index ca90c336..774a381a 100644 --- a/packages/solid-crs-manage/lib/features/authenticate/authenticate-setup.component.spec.ts +++ b/packages/solid-crs-manage/lib/features/authenticate/authenticate-setup.component.spec.ts @@ -1,10 +1,15 @@ -import { Collection, CollectionMemoryStore, CollectionObject, CollectionObjectMemoryStore, ConsoleLogger, LoggerLevel, SolidMockService } from '@netwerk-digitaal-erfgoed/solid-crs-core'; +/* eslint-disable @typescript-eslint/dot-notation */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { SolidSDKService } from '@digita-ai/inrupt-solid-service'; +import { Collection, CollectionObject, CollectionObjectMemoryStore, CollectionSolidStore } from '@netwerk-digitaal-erfgoed/solid-crs-core'; import { interpret, Interpreter } from 'xstate'; -import { ClickedAdministratorTypeEvent, ClickedInstitutionTypeEvent } from '../../app.events'; -import { AppContext, appMachine } from '../../app.machine'; +import { ClickedAdministratorTypeEvent, ClickedCreatePodEvent, ClickedInstitutionTypeEvent, ClickedLogoutEvent, SetProfileEvent } from '../../app.events'; +import { AppContext, AppDataStates, appMachine, AppRootStates } from '../../app.machine'; +import * as services from '../../app.services'; import { AuthenticateSetupComponent } from './authenticate-setup.component'; -const solid = new SolidMockService(new ConsoleLogger(LoggerLevel.silly, LoggerLevel.silly)); +let solidService: SolidSDKService; +let collectionStore: CollectionSolidStore; describe('AuthenticateSetupComponent', () => { @@ -19,22 +24,6 @@ describe('AuthenticateSetupComponent', () => { distribution: 'test-uri', }; - const collection2: Collection = { - uri: 'collection-uri-1', - name: 'Collection 1', - description: 'This is collection 1', - objectsUri: 'test-uri', - distribution: 'test-uri', - }; - - const collection3: Collection = { - uri: 'collection-uri-2', - name: 'Collection 2', - description: 'This is collection 2', - objectsUri: 'test-uri', - distribution: 'test-uri', - }; - const object1: CollectionObject = { uri: 'object-uri-1', name: 'Object 1', @@ -46,14 +35,21 @@ describe('AuthenticateSetupComponent', () => { collection: 'collection-uri-1', }; - beforeEach(() => { + beforeEach(async () => { + + solidService = { + getStorages: jest.fn(async () => [ 'https://storage.uri/' ]), + } as any; + + collectionStore = { + getInstanceForClass: jest.fn(async () => undefined), + } as any; + + (services.createPod as any) = jest.fn(async () => ({})); machine = interpret(appMachine( - solid, - new CollectionMemoryStore([ - collection2, - collection3, - ]), + solidService, + collectionStore, new CollectionObjectMemoryStore([ object1, ]), @@ -63,11 +59,20 @@ describe('AuthenticateSetupComponent', () => { .withContext({ alerts: [], session: { webId: 'lorem' }, + profile: { + name: 'Lea Peeters', + uri: 'https://web.id/', + }, })); + machine.onTransition(async () => await component.updateComplete); + component = window.document.createElement('nde-authenticate-setup') as AuthenticateSetupComponent; component.actor = machine; + machine.start(); + window.document.body.appendChild(component); + await component.updateComplete; }); @@ -83,50 +88,174 @@ describe('AuthenticateSetupComponent', () => { }); - it('should show two buttons', async () => { + describe('admin setup', () => { - machine.start(); - window.document.body.appendChild(component); - await component.updateComplete; + beforeEach(() => { + + // set correct data state () + machine.start(); + + machine.send(new SetProfileEvent()); + + }); + + it('should show two buttons', (done) => { + + machine.onTransition((state) => { + + if (state.matches({ [AppRootStates.DATA]: AppDataStates.DETERMINING_POD_TYPE })) { + + const buttons = window.document.body.getElementsByTagName('nde-authenticate-setup')[0].shadowRoot.querySelector('div.form-container').children; + expect(buttons).toBeTruthy(); + expect(buttons.length).toEqual(2); + done(); + + } + + }); + + machine.start(); + + }); + + it('should send ClickedAdministratorTypeEvent when admin button is clicked', async () => { + + machine.send(new ClickedCreatePodEvent()); + component.actor.send = jest.fn(); + window.document.body.appendChild(component); + await component.updateComplete; + + machine.onTransition(async (state) => { + + await component.updateComplete; + + if (state.matches({ [AppRootStates.DATA]: AppDataStates.DETERMINING_POD_TYPE })) { + + const button = window.document.body.getElementsByTagName('nde-authenticate-setup')[0].shadowRoot + .querySelector('div.form-container').children[0] as HTMLButtonElement; + + button.click(); + expect(component.actor.send).toHaveBeenCalledWith(new ClickedAdministratorTypeEvent()); - const buttons = window.document.body.getElementsByTagName('nde-authenticate-setup')[0].shadowRoot.querySelector('div.form-container').children; + } - expect(buttons).toBeTruthy(); - expect(buttons.length).toEqual(2); + }); + + }); + + it('should send ClickedInstitutionTypeEvent when institution button is clicked', async () => { + + machine.send(new ClickedCreatePodEvent()); + component.actor.send = jest.fn(); + window.document.body.appendChild(component); + await component.updateComplete; + + machine.onTransition(async (state) => { + + await component.updateComplete; + + if (state.matches({ [AppRootStates.DATA]: AppDataStates.DETERMINING_POD_TYPE })) { + + const button = component.shadowRoot.querySelector('div.form-container').children[1] as HTMLButtonElement; + + button.click(); + expect(component.actor.send).toHaveBeenCalledWith(new ClickedInstitutionTypeEvent()); + + } + + }); + + }); }); - it('should send ClickedAdministratorTypeEvent when admin button is clicked', async () => { + describe('pod creation', () => { - machine.start(); - window.document.body.appendChild(component); - await component.updateComplete; + beforeEach(async () => { + + solidService.getStorages = jest.fn(async () => [ ]); + + machine = interpret(appMachine( + solidService, + collectionStore, + new CollectionObjectMemoryStore([ + object1, + ]), + collection1, + object1 + ) + .withContext({ + alerts: [], + session: { webId: 'lorem' }, + profile: { + name: 'Lea Peeters', + uri: 'https://web.id/', + }, + })); + + component.actor = machine; + + machine.start(); + + machine.send(new SetProfileEvent()); - const button = window.document.body.getElementsByTagName('nde-authenticate-setup')[0].shadowRoot - .querySelector('div.form-container').children[0] as HTMLButtonElement; + }); - machine.send = jest.fn(); + it('should show two buttons', async () => { - button.click(); + machine.onTransition(async (state) => { - expect(machine.send).toHaveBeenCalledWith(new ClickedAdministratorTypeEvent()); + if (state.matches({ [AppRootStates.DATA]: AppDataStates.AWAITING_POD_CREATION })) { + + await component.updateComplete; + const buttons = component.shadowRoot.querySelectorAll('button'); + expect(buttons).toBeTruthy(); + expect(buttons.length).toEqual(2); + + } + + }); + + }); }); - it('should send ClickedInstitutionTypeEvent when institution button is clicked', async () => { + describe('onClickedCreatePod', () => { - machine.start(); + it('should send ClickedCreatePodEvent to machine', async () => { + + machine.send = jest.fn(); + component['onClickedCreatePod'](); + expect(machine.send).toHaveBeenCalledWith(new ClickedCreatePodEvent()); + + }); + + }); + + describe('onClickedCancel', () => { + + it('should send ClickedLogoutEvent to machine', async () => { + + machine.send = jest.fn(); + component['onClickedCancel'](); + expect(machine.send).toHaveBeenCalledWith(new ClickedLogoutEvent()); + + }); + + }); + + it('should not render anything when between states', async () => { + + // dont wait for getStorages to complete window.document.body.appendChild(component); await component.updateComplete; - const button = window.document.body.getElementsByTagName('nde-authenticate-setup')[0].shadowRoot - .querySelector('div.form-container').children[1] as HTMLButtonElement; - - machine.send = jest.fn(); + [ ... component.shadowRoot.children ].forEach((child) => { - button.click(); + // component should only have style elements, no other elements + expect(child).toBeInstanceOf(HTMLStyleElement); - expect(machine.send).toHaveBeenCalledWith(new ClickedInstitutionTypeEvent()); + }); }); diff --git a/packages/solid-crs-manage/lib/features/authenticate/authenticate-setup.component.ts b/packages/solid-crs-manage/lib/features/authenticate/authenticate-setup.component.ts index 1b6fd7bd..d7573f7b 100644 --- a/packages/solid-crs-manage/lib/features/authenticate/authenticate-setup.component.ts +++ b/packages/solid-crs-manage/lib/features/authenticate/authenticate-setup.component.ts @@ -1,12 +1,13 @@ -import { html, property, unsafeCSS, css, TemplateResult, CSSResult } from 'lit-element'; +import { html, property, unsafeCSS, css, TemplateResult, CSSResult, internalProperty } from 'lit-element'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { Logger, Translator } from '@netwerk-digitaal-erfgoed/solid-crs-core'; -import { Interpreter } from 'xstate'; +import { Interpreter, State } from 'xstate'; import { RxLitElement } from 'rx-lit'; import { unsafeSVG } from 'lit-html/directives/unsafe-svg'; import { Theme, Identity } from '@netwerk-digitaal-erfgoed/solid-crs-theme'; -import { ClickedAdministratorTypeEvent, ClickedInstitutionTypeEvent } from '../../app.events'; -import { AppContext } from '../../app.machine'; +import { from } from 'rxjs'; +import { ClickedAdministratorTypeEvent, ClickedCreatePodEvent, ClickedInstitutionTypeEvent, ClickedLogoutEvent } from '../../app.events'; +import { AppContext, AppDataStates, AppRootStates } from '../../app.machine'; /** * The first time setup page of the authenticate process. @@ -33,6 +34,30 @@ export class AuthenticateSetupComponent extends RxLitElement { @property({ type: Object }) public actor: Interpreter; + /** + * The state of this component. + */ + @internalProperty() + state?: State; + + protected firstUpdated(): void { + + this.subscribe('state', from(this.actor)); + + } + + private onClickedCreatePod = () => { + + this.actor.send(new ClickedCreatePodEvent()); + + }; + + private onClickedCancel = () => { + + this.actor.send(new ClickedLogoutEvent()); + + }; + /** * Renders the component as HTML. * @@ -40,7 +65,9 @@ export class AuthenticateSetupComponent extends RxLitElement { */ render(): TemplateResult { - return html` + return this.state?.matches({ [AppRootStates.DATA]: AppDataStates.DETERMINING_POD_TYPE }) + // admin - institution buttons + ? html`

${this.translator?.translate('authenticate.pages.setup.title')}

@@ -72,11 +99,24 @@ export class AuthenticateSetupComponent extends RxLitElement {
+ ` + // create pod buttons + : this.state?.matches({ [AppRootStates.DATA]: AppDataStates.AWAITING_POD_CREATION }) ? html` + +
+

${this.translator?.translate('authenticate.pages.no-pod.title')}

+
-
-

${unsafeHTML(this.translator?.translate('authenticate.pages.login.create-webid'))}

+
+

${this.translator?.translate('authenticate.pages.no-pod.subtitle.no-storage')}

+

${unsafeHTML(this.translator?.translate('authenticate.pages.no-pod.subtitle.add-storage'))}

- `; + +
+ + +
+ ` : html``; } @@ -94,11 +134,12 @@ export class AuthenticateSetupComponent extends RxLitElement { justify-content: center; align-items: center; background-color: var(--colors-primary-dark); + color: var(--colors-foreground-inverse); + gap: var(--gap-huge); } :host > * { - margin-bottom: var(--gap-large); - width: 50%; + width: 500px; } .title-container { @@ -107,7 +148,6 @@ export class AuthenticateSetupComponent extends RxLitElement { flex-direction: row; justify-content: center; align-items: center; - color: var(--colors-foreground-inverse); } .title-container svg { @@ -129,10 +169,14 @@ export class AuthenticateSetupComponent extends RxLitElement { flex-direction: column; justify-content: center; align-items: stretch; + gap: var(--gap-normal); } - .form-container > * { - margin-bottom: var(--gap-large); + .button-container { + display: flex; + flex-direction: column; + justify-content: center; + gap: var(--gap-normal); } nde-large-card { @@ -142,6 +186,12 @@ export class AuthenticateSetupComponent extends RxLitElement { svg { stroke: var(--colors-foreground-light) !important; } + + p { + text-align: center; + font-size: var(--font-size-small); + margin: 0; + } `, ]; diff --git a/packages/solid-crs-manage/lib/features/collection/collection-root.component.ts b/packages/solid-crs-manage/lib/features/collection/collection-root.component.ts index b0a62bf9..4f6ed53c 100644 --- a/packages/solid-crs-manage/lib/features/collection/collection-root.component.ts +++ b/packages/solid-crs-manage/lib/features/collection/collection-root.component.ts @@ -5,7 +5,7 @@ import { map } from 'rxjs/operators'; import { from } from 'rxjs'; import { ActorRef, Interpreter, State } from 'xstate'; import { RxLitElement } from 'rx-lit'; -import { Collection as CollectionIcon, Connect, Cross, Empty, Object as ObjectIcon, Open, Plus, Save, Theme, Trash } from '@netwerk-digitaal-erfgoed/solid-crs-theme'; +import { Collection as CollectionIcon, Cross, Empty, Object as ObjectIcon, Open, Plus, Save, Theme, Trash } from '@netwerk-digitaal-erfgoed/solid-crs-theme'; import { unsafeSVG } from 'lit-html/directives/unsafe-svg'; import { DismissAlertEvent } from '../../app.events'; import { ObjectEvents } from '../object/object.events'; diff --git a/packages/solid-crs-manage/lib/public/nl-NL.json b/packages/solid-crs-manage/lib/public/nl-NL.json index 1bbac642..e4e59cfc 100644 --- a/packages/solid-crs-manage/lib/public/nl-NL.json +++ b/packages/solid-crs-manage/lib/public/nl-NL.json @@ -224,7 +224,15 @@ "separator": "of", "create-webid": "Nog geen WebID? Klik hier." }, - + "no-pod": { + "title": "Je hebt nog geen pod.", + "subtitle": { + "no-storage": "We konden geen pod vinden voor je WebID.", + "add-storage": "Voeg handmatig een pod toe of maak een pod aan bij NDE." + }, + "button-create-pod": "Maak een pod aan bij NDE", + "button-cancel": "Annuleren" + }, "setup": { "title": "Hoe wil je deze pod gebruiken?", "button-administrator": { diff --git a/packages/solid-crs-manage/package-lock.json b/packages/solid-crs-manage/package-lock.json index 88e112b3..e5e48767 100644 --- a/packages/solid-crs-manage/package-lock.json +++ b/packages/solid-crs-manage/package-lock.json @@ -660,13 +660,13 @@ "dev": true }, "@digita-ai/dgt-components": { - "version": "0.10.0", - "resolved": "https://npm.pkg.github.com/download/@digita-ai/dgt-components/0.10.0/8e01693a6350086325cc4a161a37655c29929b3b68a727ac04c0cbbb11fe4a9e", - "integrity": "sha512-zgpJqZBPb9AqZbByQDd2t8BC/GesMnHUhYu1Q6Jm2Y8X+RfQecu1EgKDFdp240+dGQU2cbor6ei37f5jMt63bQ==", + "version": "0.11.1", + "resolved": "https://npm.pkg.github.com/download/@digita-ai/dgt-components/0.11.1/0ce32e9b8451f12675da7d4ce703a29bc432ceda9b027a2b883b912c50037227", + "integrity": "sha512-vsKXyl/C8DtzQAAmDsCfWUsTfsWYcdOJkqR6pFHcNQ7Ppstp9PzYEIno1UrGWCCigzA14Z1O7tGglWbFyBzWSg==", "requires": { "@appnest/lit-translate": "^1.1.18", - "@digita-ai/dgt-theme": "0.10.0", - "@digita-ai/dgt-utils": "0.10.0", + "@digita-ai/dgt-theme": "0.11.1", + "@digita-ai/dgt-utils": "0.11.1", "@digita-ai/inrupt-solid-service": "^0.9.2", "@digita-ai/semcom-core": "^0.9.5", "@digita-ai/semcom-sdk": "^0.9.5", @@ -726,16 +726,16 @@ } }, "@digita-ai/semcom-core": { - "version": "0.9.7", - "resolved": "https://npm.pkg.github.com/download/@digita-ai/semcom-core/0.9.7/5a8cfc4b98490a80d34263503a5b10db30dc03994e86cd17e2614b9d08e41788", - "integrity": "sha512-+CJOd68u7qohy5xu3xUImJR5KS4QVroubT/eNXcDXMrChkK73dfh3wzF99+vWhav0ez05WrKPPaVwbyfszXXEg==" + "version": "0.9.8", + "resolved": "https://npm.pkg.github.com/download/@digita-ai/semcom-core/0.9.8/4890dc24ceba8fb377c07dce43fe7b5bd13288b65e0813315071bc54d027fc6b", + "integrity": "sha512-mKIDGhxWjNxV9XGf3sLArWeqAiNvkQlmDKSzeJ3rTWM2xD0iJpEIH9SjF8evZ0deIA/RPTsEijFEE9KYDc/Vvw==" }, "@digita-ai/semcom-sdk": { - "version": "0.9.7", - "resolved": "https://npm.pkg.github.com/download/@digita-ai/semcom-sdk/0.9.7/1bde4fcff3e21d84bf34c1f639e33c9732ffbd49efdfb902c511a53d19306fa9", - "integrity": "sha512-gZ9HPcMK+xa4ojR29/NpSK1uREPMK3z1APPx+5lQ5hQMqlEzvKq1JLV9rFsvfJl/8l0jvhWoNKRohOP8S9jBiw==", + "version": "0.9.8", + "resolved": "https://npm.pkg.github.com/download/@digita-ai/semcom-sdk/0.9.8/5b8c07f72343a626b4e359befdfbc340c62a7a96e48f89d76703da3d790fd74b", + "integrity": "sha512-GWYv33tKckBcWAkCyE+e2HaF+She0wrdvUWAMF3aDdn2ML6apftgBfYgIXIbPq+F/sM4Pt7xD62qxtYe3EamHg==", "requires": { - "@digita-ai/semcom-core": "0.9.7", + "@digita-ai/semcom-core": "0.9.8", "buffer": "6.0.3", "jssha": "^3.2.0", "n3": "1.10.0" @@ -814,26 +814,26 @@ } }, "@jest/environment": { - "version": "27.4.4", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.4.4.tgz", - "integrity": "sha512-q+niMx7cJgt/t/b6dzLOh4W8Ef/8VyKG7hxASK39jakijJzbFBGpptx3RXz13FFV7OishQ9lTbv+dQ5K3EhfDQ==", + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.4.6.tgz", + "integrity": "sha512-E6t+RXPfATEEGVidr84WngLNWZ8ffCPky8RqqRK6u1Bn0LK92INe0MDttyPl/JOzaq92BmDzOeuqk09TvM22Sg==", "requires": { - "@jest/fake-timers": "^27.4.2", + "@jest/fake-timers": "^27.4.6", "@jest/types": "^27.4.2", "@types/node": "*", - "jest-mock": "^27.4.2" + "jest-mock": "^27.4.6" } }, "@jest/fake-timers": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.4.2.tgz", - "integrity": "sha512-f/Xpzn5YQk5adtqBgvw1V6bF8Nx3hY0OIRRpCvWcfPl0EAjdqWPdhH3t/3XpiWZqtjIEHDyMKP9ajpva1l4Zmg==", + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.4.6.tgz", + "integrity": "sha512-mfaethuYF8scV8ntPpiVGIHQgS0XIALbpY2jt2l7wb/bvq4Q5pDLk4EP4D7SAvYT1QrPOPVZAtbdGAOOyIgs7A==", "requires": { "@jest/types": "^27.4.2", "@sinonjs/fake-timers": "^8.0.1", "@types/node": "*", - "jest-message-util": "^27.4.2", - "jest-mock": "^27.4.2", + "jest-message-util": "^27.4.6", + "jest-mock": "^27.4.6", "jest-util": "^27.4.2" } }, @@ -937,23 +937,23 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "jest-environment-jsdom": { - "version": "27.4.4", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.4.4.tgz", - "integrity": "sha512-cYR3ndNfHBqQgFvS1RL7dNqSvD//K56j/q1s2ygNHcfTCAp12zfIromO1w3COmXrxS8hWAh7+CmZmGCIoqGcGA==", + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.4.6.tgz", + "integrity": "sha512-o3dx5p/kHPbUlRvSNjypEcEtgs6LmvESMzgRFQE6c+Prwl2JLA4RZ7qAnxc5VM8kutsGRTB15jXeeSbJsKN9iA==", "requires": { - "@jest/environment": "^27.4.4", - "@jest/fake-timers": "^27.4.2", + "@jest/environment": "^27.4.6", + "@jest/fake-timers": "^27.4.6", "@jest/types": "^27.4.2", "@types/node": "*", - "jest-mock": "^27.4.2", + "jest-mock": "^27.4.6", "jest-util": "^27.4.2", "jsdom": "^16.6.0" } }, "jest-message-util": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.4.2.tgz", - "integrity": "sha512-OMRqRNd9E0DkBLZpFtZkAGYOXl6ZpoMtQJWTAREJKDOFa0M6ptB7L67tp+cszMBkvSgKOhNtQp2Vbcz3ZZKo/w==", + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.4.6.tgz", + "integrity": "sha512-0p5szriFU0U74czRSFjH6RyS7UYIAkn/ntwMuOwTGWrQIOh5NzXXrq72LOqIkJKKvFbPq+byZKuBz78fjBERBA==", "requires": { "@babel/code-frame": "^7.12.13", "@jest/types": "^27.4.2", @@ -961,15 +961,15 @@ "chalk": "^4.0.0", "graceful-fs": "^4.2.4", "micromatch": "^4.0.4", - "pretty-format": "^27.4.2", + "pretty-format": "^27.4.6", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "jest-mock": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.4.2.tgz", - "integrity": "sha512-PDDPuyhoukk20JrQKeofK12hqtSka7mWH0QQuxSNgrdiPsrnYYLS6wbzu/HDlxZRzji5ylLRULeuI/vmZZDrYA==", + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.4.6.tgz", + "integrity": "sha512-kvojdYRkst8iVSZ1EJ+vc1RRD9llueBjKzXzeCytH3dMM7zvPV/ULcfI2nr0v0VUgm3Bjt3hBCQvOeaBz+ZTHw==", "requires": { "@jest/types": "^27.4.2", "@types/node": "*" @@ -1007,11 +1007,10 @@ } }, "pretty-format": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.2.tgz", - "integrity": "sha512-p0wNtJ9oLuvgOQDEIZ9zQjZffK7KtyR6Si0jnXULIDwrlNF8Cuir3AZP0hHv0jmKuNN/edOnbMjnzd4uTcmWiw==", + "version": "27.4.6", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.6.tgz", + "integrity": "sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g==", "requires": { - "@jest/types": "^27.4.2", "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" @@ -1043,17 +1042,18 @@ } }, "@digita-ai/dgt-theme": { - "version": "0.10.0", - "resolved": "https://npm.pkg.github.com/download/@digita-ai/dgt-theme/0.10.0/f8f1d1afcc92c143c1db10a64b6e22f51c99e37133a80465612d39875c6338bd", - "integrity": "sha512-lkFBvr+9nhneIct1TboxWEbHq+zwT3hFo8K7L5skG9yBuX9fMucXneIoo5aGX0F8JJK//0SYphVRaL/8m7JkFA==" + "version": "0.11.1", + "resolved": "https://npm.pkg.github.com/download/@digita-ai/dgt-theme/0.11.1/0719862c4794e66e318ed987c6079b7cf86bbf8b74b898f8b63fa797a07c3c5b", + "integrity": "sha512-5hCFOXTWfYfECQTWMVu7SyHcIbi8Bpg5UXxIosfgvvp0+qyILFDBAAIYz637mpKTj+hBrlQjNV0a5XTr5+jZCQ==" }, "@digita-ai/dgt-utils": { - "version": "0.10.0", - "resolved": "https://npm.pkg.github.com/download/@digita-ai/dgt-utils/0.10.0/5686b5d650eb81ac14763ec34fa6223e08a602fee92a1f673518da4337176349", - "integrity": "sha512-FcWCnYUhWsCmTMpFzCs8dF7zyA0OQuxoIhCtCkRqiFMIjRTIPMjUzo7IBCCKfwpv1XXMuPTudzPZglTGmk1cZA==", + "version": "0.11.1", + "resolved": "https://npm.pkg.github.com/download/@digita-ai/dgt-utils/0.11.1/ffde3a6fc2ced867c3b4688d24a4f2e520ecc887baf349a6014b496cd5105ff1", + "integrity": "sha512-5Opm6E46WrGsL4ueGiS0kiSJD0k4v5pkYpNitFcQ15HC5okRKG6pel0dob7otAokTvtiJcBYbE9xwVTNzbwt2g==", "requires": { "@types/node": "^14.14.14", "rxjs": "^7.4.0", + "xstate": "^4.26.1", "zone.js": "~0.8.26" } }, @@ -1787,7 +1787,11 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" } }, "chalk": { diff --git a/packages/solid-crs-manage/package.json b/packages/solid-crs-manage/package.json index 2235defa..85a170a1 100644 --- a/packages/solid-crs-manage/package.json +++ b/packages/solid-crs-manage/package.json @@ -29,7 +29,7 @@ "posttest": "jest-coverage-thresholds-bumper --silent --coverage-summary-path coverage-summary.json" }, "dependencies": { - "@digita-ai/dgt-components": "0.10.0", + "@digita-ai/dgt-components": " 0.11.1", "@digita-ai/inrupt-solid-service": "^0.11.0", "@digita-ai/semcom-core": "0.4.1", "@digita-ai/semcom-sdk": "0.4.1", @@ -91,10 +91,10 @@ ], "coverageThreshold": { "global": { - "statements": 84.98, - "branches": 85.21, - "lines": 86.1, - "functions": 71.25 + "statements": 85.67, + "branches": 85.14, + "lines": 86.9, + "functions": 71.75 } }, "automock": false, @@ -116,4 +116,4 @@ }, "maxWorkers": 4 } -} +} \ No newline at end of file diff --git a/packages/solid-crs-pods/Dockerfile b/packages/solid-crs-pods/Dockerfile index 85fab3dd..195d075e 100644 --- a/packages/solid-crs-pods/Dockerfile +++ b/packages/solid-crs-pods/Dockerfile @@ -16,8 +16,9 @@ WORKDIR /community-server # Install app dependencies ARG NPM_TOKEN -COPY package.json /community-server/ +COPY . /community-server/ RUN npm install --unsafe-perm +RUN npm run build # Bundle app source COPY . /community-server @@ -28,4 +29,4 @@ COPY data/hetlageland/ /tmp/css/hetlageland/ # Expose ports. EXPOSE 80 -CMD node ./node_modules/.bin/community-solid-server -b ${BASE_URL} -p ${PORT} -c ${CONFIG} --rootFilePath /tmp/css +CMD node ./node_modules/.bin/community-solid-server -m . -b ${BASE_URL} -p ${PORT} -c ${CONFIG} --rootFilePath /tmp/css diff --git a/packages/solid-crs-theme/lib/elements/buttons.css b/packages/solid-crs-theme/lib/elements/buttons.css index efb02fa9..0e5111e6 100644 --- a/packages/solid-crs-theme/lib/elements/buttons.css +++ b/packages/solid-crs-theme/lib/elements/buttons.css @@ -1,7 +1,6 @@ button { border: none; background-color: var(--colors-primary-dark); - text-transform: uppercase; padding: var(--gap-small); color: var(--colors-foreground-inverse); cursor: pointer;