Skip to content

Commit

Permalink
feat: improve webid validation (#102)
Browse files Browse the repository at this point in the history
* feat: add webId validation to authenticate machine WIP

* feat: show validation messages in authenticate component WIP

* test: update tests

* fix: allow styling of validation alert

* fix: disable trusted issuers in demo

* fix: change position of alert
  • Loading branch information
lem-onade authored Nov 22, 2021
1 parent 647b879 commit 170a649
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 89 deletions.
24 changes: 23 additions & 1 deletion packages/dgt-components/demo/demo-authenticate.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,39 @@ import { Theme } from '@digita-ai/dgt-theme';
import { SolidSDKService } from '@digita-ai/inrupt-solid-service';
import { AuthenticateComponent } from '../lib/components/authentication/authenticate.component';
import { hydrate } from '../lib/util/hydrate';
import { WebIdValidator } from '../lib/components/authentication/authenticate.machine';
import { Translator } from '../lib/services/i18n/translator';

export class DemoAuthenticateComponent extends RxLitElement {

private solidService = new SolidSDKService('DemoAuthenticateComponent');
private trustedIssuers = [ 'https://inrupt.net/' ];
private translations = {
'common.webid-validation.invalid-uri': 'The URL of the entered WebID is invalid',
}
private translator: Translator = {
translate: (key: string) => this.translations[key],
} as any

// example validator
private webIdValidator: WebIdValidator = async (webId: string) => {
let results: string[] = [];
try {
new URL(webId);
} catch {
results.push('common.webid-validation.invalid-uri');
}
return results;
}

onAuthenticated = (): void => { alert('Demo event: authenticated') };
onNoTrust = (): void => { alert('Demo event: no trusted issuers') };
onCreateWebId = (): void => { alert('Demo event: create webid') };

constructor() {
super();
customElements.define('auth-flow', hydrate(AuthenticateComponent)(this.solidService, this.trustedIssuers));
// customElements.define('auth-flow', hydrate(AuthenticateComponent)(this.solidService, this.trustedIssuers, this.webIdValidator));
customElements.define('auth-flow', hydrate(AuthenticateComponent)(this.solidService, undefined, this.webIdValidator));

}
/**
Expand All @@ -28,9 +48,11 @@ export class DemoAuthenticateComponent extends RxLitElement {

return html`
<auth-flow
hideCreateNewWebId
@authenticated="${this.onAuthenticated}"
@no-trust="${this.onNoTrust}"
@create-webid="${this.onCreateWebId}"
.translator="${this.translator}"
>
<h1 slot="beforeIssuers">Select an identity provider to log in</h1>
<h1 slot="beforeWebId">Enter your WebID</h1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('AlertComponent', () => {

beforeEach(() => {

component = window.document.createElement('nde-alert') as AlertComponent;
component = window.document.createElement('alert-component') as AlertComponent;

});

Expand All @@ -35,7 +35,7 @@ describe('AlertComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

const message = window.document.body.getElementsByTagName('nde-alert')[0].shadowRoot.querySelector('.message').innerHTML.replace(/<!---->/g, '');
const message = window.document.body.getElementsByTagName('alert-component')[0].shadowRoot.querySelector('.message').innerHTML.replace(/<!---->/g, '');

expect(message).toBe('Foo');

Expand All @@ -53,7 +53,7 @@ describe('AlertComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

const message = window.document.body.getElementsByTagName('nde-alert')[0].shadowRoot.querySelector('.message').innerHTML.replace(/<!---->/g, '');
const message = window.document.body.getElementsByTagName('alert-component')[0].shadowRoot.querySelector('.message').innerHTML.replace(/<!---->/g, '');

expect(message).toBe(component.alert.message);

Expand All @@ -71,7 +71,7 @@ describe('AlertComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

const message = window.document.body.getElementsByTagName('nde-alert')[0].shadowRoot.querySelector('.message').innerHTML.replace(/<!---->/g, '');
const message = window.document.body.getElementsByTagName('alert-component')[0].shadowRoot.querySelector('.message').innerHTML.replace(/<!---->/g, '');

expect(message.trim()).toBe(component.alert.message);

Expand All @@ -87,7 +87,7 @@ describe('AlertComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

const alert = window.document.body.getElementsByTagName('nde-alert')[0].shadowRoot.querySelector(`.alert.${type}`);
const alert = window.document.body.getElementsByTagName('alert-component')[0].shadowRoot.querySelector(`.alert.${type}`);

expect(alert).toBeTruthy();

Expand All @@ -103,7 +103,7 @@ describe('AlertComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

const alert = window.document.body.getElementsByTagName('nde-alert')[0].shadowRoot.querySelector('.alert.warning');
const alert = window.document.body.getElementsByTagName('alert-component')[0].shadowRoot.querySelector('.alert.warning');

expect(alert).toBeTruthy();

Expand All @@ -121,7 +121,7 @@ describe('AlertComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

const dismiss = window.document.body.getElementsByTagName('nde-alert')[0].shadowRoot.querySelector('.dismiss') as HTMLElement;
const dismiss = window.document.body.getElementsByTagName('alert-component')[0].shadowRoot.querySelector('.dismiss') as HTMLElement;
dismiss.click();

expect(component.dismiss).toHaveBeenCalledTimes(1);
Expand All @@ -140,7 +140,7 @@ describe('AlertComponent', () => {
window.document.body.appendChild(component);
await component.updateComplete;

const dismiss = window.document.body.getElementsByTagName('nde-alert')[0].shadowRoot.querySelector('.dismiss') as HTMLElement;
const dismiss = window.document.body.getElementsByTagName('alert-component')[0].shadowRoot.querySelector('.dismiss') as HTMLElement;
dismiss.click();

expect(component.dispatchEvent).toHaveBeenCalledTimes(1);
Expand Down
81 changes: 39 additions & 42 deletions packages/dgt-components/lib/components/alerts/alert.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,55 @@ export class AlertComponent extends LitElement {
/**
* The component's logger.
*/
@property({ type: DGTLoggerService })
public logger: DGTLoggerService;

@property({ type: DGTLoggerService }) public logger: DGTLoggerService;
/**
* The component's translator.
*/
@property({ type: Translator })
public translator: Translator;
@property({ type: Translator }) translator?: Translator;

/**
* The collection which will be rendered by the component.
*/
@property({ type: Object })
public alert: Alert;

/**
* Dispatches an event to dismiss the alert.
*/
dismiss() {

this.logger?.debug(AlertComponent.name, 'Dismissing alert', this.alert);

if (!this.alert) {

throw new DGTErrorArgument('Argument this.alert should be set.', this.alert);

}

this.dispatchEvent(new CustomEvent<Alert>('dismiss', { detail:this.alert }));

}

/**
* Renders the component as HTML.
*
* @returns The rendered HTML of the component.
*/
render() {

const message = this.translator ? this.translator.translate(this.alert?.message) : this.alert?.message;
const type = this.alert && this.alert.type ? this.alert.type : 'warning';

return html`
<div part="validation-alert" class="alert ${ type }">
<div class="icon">${ unsafeSVG(Bell) }</div>
<div class="message">${ message }</div>
<div class="dismiss" @click="${ this.dismiss }">${ unsafeSVG(Cross) }</div>
</div>
`;

}

/**
* The styles associated with the component.
*/
Expand Down Expand Up @@ -83,43 +117,6 @@ export class AlertComponent extends LitElement {

}

/**
* Dispatches an event to dismiss the alert.
*/
dismiss() {

this.logger?.debug(AlertComponent.name, 'Dismissing alert', this.alert);

if (!this.alert) {

throw new DGTErrorArgument('Argument this.alert should be set.', this.alert);

}

this.dispatchEvent(new CustomEvent<Alert>('dismiss', { detail:this.alert }));

}

/**
* Renders the component as HTML.
*
* @returns The rendered HTML of the component.
*/
render() {

const message = this.translator ? this.translator.translate(this.alert?.message) : this.alert?.message;
const type = this.alert && this.alert.type ? this.alert.type : 'warning';

return html`
<div class="alert ${ type }">
<div class="icon">${ unsafeSVG(Bell) }</div>
<div class="message">${ message }</div>
<div class="dismiss" @click="${ this.dismiss }">${ unsafeSVG(Cross) }</div>
</div>
`;

}

}

export default AlertComponent;
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { ProviderListComponent } from '../provider/provider-list.component';
import { SeparatorComponent } from '../separator/separator.component';
import { LoadingComponent } from '../loading/loading.component';
import { define } from '../../util/define';
import { Translator } from '../../services/i18n/translator';
import { WebIdComponent } from './webid.component';
import { AuthenticateContext, AuthenticateEvent, authenticateMachine, AuthenticateState, AuthenticateStates, AuthenticateStateSchema, SelectedIssuerEvent, WebIdEnteredEvent } from './authenticate.machine';
import { AuthenticateContext, AuthenticateEvent, AuthenticateEvents, authenticateMachine, AuthenticateState, AuthenticateStates, AuthenticateStateSchema, SelectedIssuerEvent, WebIdEnteredEvent, WebIdValidator } from './authenticate.machine';

export class AuthenticateComponent extends RxLitElement {

Expand All @@ -27,6 +28,8 @@ export class AuthenticateComponent extends RxLitElement {
@property({ type: Boolean }) hideWebId = false;
@property({ type: Boolean }) hideIssuers = false;
@property({ type: Boolean }) hideCreateNewWebId = false;
@property() webIdValidationResults: string[];
@property({ type: Translator }) translator?: Translator;

@property({ type: Array }) trusted: string[];

Expand All @@ -41,7 +44,7 @@ export class AuthenticateComponent extends RxLitElement {
@property({ type: String }) textNoWebId = 'No WebID yet?';
@property({ type: String }) textButton = 'Connect';

constructor(solidService: SolidService, trustedIssuers?: string[]) {
constructor(solidService: SolidService, trustedIssuers?: string[], webIdValidator?: WebIdValidator) {

super();

Expand All @@ -50,14 +53,34 @@ export class AuthenticateComponent extends RxLitElement {
define('separator-component', SeparatorComponent);
define('loading-component', LoadingComponent);

this.machine = createMachine(authenticateMachine(solidService)).withContext({ trusted: trustedIssuers });
this.machine = createMachine(authenticateMachine(solidService))
.withContext({
trusted: trustedIssuers,
webIdValidator,
});

// eslint-disable-next-line no-console
this.actor = interpret(this.machine, { devTools: true }).onTransition((state) => console.log(state.value));

this.subscribe('state', from(this.actor));
this.subscribe('issuers', from(this.actor).pipe(map((state) => state.context.issuers)));

this.subscribe('webIdValidationResults', from(this.actor).pipe(map((state) => {

if (state.event.type === AuthenticateEvents.LOGIN_ERROR) {

this.dispatchEvent(new CustomEvent('authenticate-error', { detail: state.event.results }));

return state.event.results;

} else {

return this.webIdValidationResults;

}

})));

this.actor.onDone((event: DoneEvent) => {

if (event.data.session) this.dispatchEvent(new CustomEvent('authenticated', { detail: event.data.session }));
Expand Down Expand Up @@ -100,7 +123,7 @@ export class AuthenticateComponent extends RxLitElement {
</separator-component>
<webid-form
exportparts="webid-label, webid-input, webid-create, webid-button"
exportparts="webid-label, webid-input, webid-create, webid-button, validation-alert"
?hidden="${this.hideWebId}"
?hideCreateNewWebId="${this.hideCreateNewWebId}"
@submit-webid="${this.onSubmit}"
Expand All @@ -109,6 +132,8 @@ export class AuthenticateComponent extends RxLitElement {
.textPlaceholder="${this.textWebIdPlaceholder}"
.textNoWebId="${this.textNoWebId}"
.textButton="${this.textButton}"
.validationResults="${this.webIdValidationResults}"
.translator="${this.translator}"
>
<slot name="beforeWebId" slot="before"></slot>
<slot name="afterWebId" slot="after"></slot>
Expand Down
Loading

0 comments on commit 170a649

Please sign in to comment.