Merge pull request #1818 from 4Science/CST-6782-refactor

New users might be registered in a massive way by a robot
This commit is contained in:
Tim Donohue
2022-10-04 09:53:15 -05:00
committed by GitHub
25 changed files with 795 additions and 55 deletions

View File

@@ -24,7 +24,7 @@ import 'cypress-axe';
beforeEach(() => { beforeEach(() => {
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
// This just ensures it doesn't get in the way of matching other objects in the page. // This just ensures it doesn't get in the way of matching other objects in the page.
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}'); cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}');
}); });
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test. // For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.

View File

@@ -78,6 +78,7 @@
"@nguniversal/express-engine": "^13.0.2", "@nguniversal/express-engine": "^13.0.2",
"@ngx-translate/core": "^13.0.0", "@ngx-translate/core": "^13.0.0",
"@nicky-lenaers/ngx-scroll-to": "^9.0.0", "@nicky-lenaers/ngx-scroll-to": "^9.0.0",
"@types/grecaptcha": "^3.0.4",
"angular-idle-preload": "3.0.0", "angular-idle-preload": "3.0.0",
"angulartics2": "^12.0.0", "angulartics2": "^12.0.0",
"axios": "^0.27.2", "axios": "^0.27.2",

View File

@@ -9,6 +9,8 @@ import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { RequestEntry } from './request-entry.model'; import { RequestEntry } from './request-entry.model';
import { HttpHeaders } from '@angular/common/http';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
describe('EpersonRegistrationService', () => { describe('EpersonRegistrationService', () => {
let testScheduler; let testScheduler;
@@ -79,8 +81,23 @@ describe('EpersonRegistrationService', () => {
it('should send an email registration', () => { it('should send an email registration', () => {
const expected = service.registerEmail('test@mail.org'); const expected = service.registerEmail('test@mail.org');
let headers = new HttpHeaders();
const options: HttpOptions = Object.create({});
options.headers = headers;
expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration, options));
expect(expected).toBeObservable(cold('(a|)', { a: rd }));
});
it('should send an email registration with captcha', () => {
const expected = service.registerEmail('test@mail.org', 'afreshcaptchatoken');
let headers = new HttpHeaders();
const options: HttpOptions = Object.create({});
headers = headers.append('x-recaptcha-token', 'afreshcaptchatoken');
options.headers = headers;
expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration, options));
expect(expected).toBeObservable(cold('(a|)', { a: rd })); expect(expected).toBeObservable(cold('(a|)', { a: rd }));
}); });
}); });

View File

@@ -3,15 +3,17 @@ import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { GetRequest, PostRequest } from './request.models'; import { GetRequest, PostRequest } from './request.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, find, map, skipWhile } from 'rxjs/operators'; import { filter, find, map } from 'rxjs/operators';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Registration } from '../shared/registration.model'; import { Registration } from '../shared/registration.model';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators'; import { getFirstCompletedRemoteData } from '../shared/operators';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { RegistrationResponseParsingService } from './registration-response-parsing.service'; import { RegistrationResponseParsingService } from './registration-response-parsing.service';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { HttpHeaders } from '@angular/common/http';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -51,8 +53,9 @@ export class EpersonRegistrationService {
/** /**
* Register a new email address * Register a new email address
* @param email * @param email
* @param captchaToken the value of x-recaptcha-token header
*/ */
registerEmail(email: string): Observable<RemoteData<Registration>> { registerEmail(email: string, captchaToken: string = null): Observable<RemoteData<Registration>> {
const registration = new Registration(); const registration = new Registration();
registration.email = email; registration.email = email;
@@ -60,10 +63,17 @@ export class EpersonRegistrationService {
const href$ = this.getRegistrationEndpoint(); const href$ = this.getRegistrationEndpoint();
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
if (captchaToken) {
headers = headers.append('x-recaptcha-token', captchaToken);
}
options.headers = headers;
href$.pipe( href$.pipe(
find((href: string) => hasValue(href)), find((href: string) => hasValue(href)),
map((href: string) => { map((href: string) => {
const request = new PostRequest(requestId, href, registration); const request = new PostRequest(requestId, href, registration, options);
this.requestService.send(request); this.requestService.send(request);
}) })
).subscribe(); ).subscribe();

View File

@@ -0,0 +1,25 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { GoogleRecaptchaComponent } from '../../shared/google-recaptcha/google-recaptcha.component';
import { GoogleRecaptchaService } from './google-recaptcha.service';
const PROVIDERS = [
GoogleRecaptchaService
];
const COMPONENTS = [
GoogleRecaptchaComponent
];
@NgModule({
imports: [ CommonModule ],
providers: [...PROVIDERS],
declarations: [...COMPONENTS],
exports: [...COMPONENTS]
})
/**
* This module handles google recaptcha functionalities
*/
export class GoogleRecaptchaModule {}

View File

@@ -0,0 +1,57 @@
import { GoogleRecaptchaService } from './google-recaptcha.service';
import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { NativeWindowRef } from '../services/window.service';
describe('GoogleRecaptchaService', () => {
let service: GoogleRecaptchaService;
let rendererFactory2;
let configurationDataService;
let spy: jasmine.Spy;
let scriptElementMock: any;
let cookieService;
let window;
const innerHTMLTestValue = 'mock-script-inner-html';
const document = { documentElement: { lang: 'en' } } as Document;
scriptElementMock = {
set innerHTML(newVal) { /* noop */ },
get innerHTML() { return innerHTMLTestValue; }
};
function init() {
window = new NativeWindowRef();
rendererFactory2 = jasmine.createSpyObj('rendererFactory2', {
createRenderer: observableOf('googleRecaptchaToken'),
createElement: scriptElementMock
});
configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] })
});
cookieService = jasmine.createSpyObj('cookieService', {
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
set: () => {
/* empty */
}
});
service = new GoogleRecaptchaService(cookieService, document, window, rendererFactory2, configurationDataService);
}
beforeEach(() => {
init();
});
describe('getRecaptchaToken', () => {
let result;
beforeEach(() => {
spy = spyOn(service, 'getRecaptchaToken').and.stub();
});
it('should send a Request with action', () => {
service.getRecaptchaToken('test');
expect(spy).toHaveBeenCalledWith('test');
});
});
});

View File

@@ -0,0 +1,176 @@
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { ConfigurationProperty } from '../shared/configuration-property.model';
import { isNotEmpty } from '../../shared/empty.util';
import { DOCUMENT } from '@angular/common';
import { ConfigurationDataService } from '../data/configuration-data.service';
import { RemoteData } from '../data/remote-data';
import { map, switchMap, take } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { CookieService } from '../services/cookie.service';
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
export const CAPTCHA_COOKIE = '_GRECAPTCHA';
export const CAPTCHA_NAME = 'google-recaptcha';
/**
* A GoogleRecaptchaService used to send action and get a token from REST
*/
@Injectable()
export class GoogleRecaptchaService {
private renderer: Renderer2;
/**
* A Google Recaptcha version
*/
private captchaVersionSubject$ = new BehaviorSubject<string>(null);
/**
* The Google Recaptcha Key
*/
private captchaKeySubject$ = new BehaviorSubject<string>(null);
/**
* The Google Recaptcha mode
*/
private captchaModeSubject$ = new BehaviorSubject<string>(null);
captchaKey(): Observable<string> {
return this.captchaKeySubject$.asObservable();
}
captchaMode(): Observable<string> {
return this.captchaModeSubject$.asObservable();
}
captchaVersion(): Observable<string> {
return this.captchaVersionSubject$.asObservable();
}
constructor(
private cookieService: CookieService,
@Inject(DOCUMENT) private _document: Document,
@Inject(NativeWindowService) private _window: NativeWindowRef,
rendererFactory: RendererFactory2,
private configService: ConfigurationDataService,
) {
if (this._window.nativeWindow) {
this._window.nativeWindow.refreshCaptchaScript = this.refreshCaptchaScript;
}
this.renderer = rendererFactory.createRenderer(null, null);
const registrationVerification$ = this.configService.findByPropertyName('registration.verification.enabled').pipe(
take(1),
getFirstCompletedRemoteData(),
map((res: RemoteData<ConfigurationProperty>) => {
return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0].toLowerCase() === 'true';
})
);
registrationVerification$.subscribe(registrationVerification => {
if (registrationVerification) {
this.loadRecaptchaProperties();
}
});
}
loadRecaptchaProperties() {
const recaptchaKeyRD$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe(
getFirstCompletedRemoteData(),
);
const recaptchaVersionRD$ = this.configService.findByPropertyName('google.recaptcha.version').pipe(
getFirstCompletedRemoteData(),
);
const recaptchaModeRD$ = this.configService.findByPropertyName('google.recaptcha.mode').pipe(
getFirstCompletedRemoteData(),
);
combineLatest([recaptchaVersionRD$, recaptchaModeRD$, recaptchaKeyRD$]).subscribe(([recaptchaVersionRD, recaptchaModeRD, recaptchaKeyRD]) => {
if (
this.cookieService.get('klaro-anonymous') && this.cookieService.get('klaro-anonymous')[CAPTCHA_NAME] &&
recaptchaKeyRD.hasSucceeded && recaptchaVersionRD.hasSucceeded &&
isNotEmpty(recaptchaVersionRD.payload?.values) && isNotEmpty(recaptchaKeyRD.payload?.values)
) {
const key = recaptchaKeyRD.payload?.values[0];
const version = recaptchaVersionRD.payload?.values[0];
this.captchaKeySubject$.next(key);
this.captchaVersionSubject$.next(version);
let captchaUrl;
switch (version) {
case 'v3':
if (recaptchaKeyRD.hasSucceeded && isNotEmpty(recaptchaKeyRD.payload?.values)) {
captchaUrl = this.buildCaptchaUrl(key);
this.captchaModeSubject$.next('invisible');
}
break;
case 'v2':
if (recaptchaModeRD.hasSucceeded && isNotEmpty(recaptchaModeRD.payload?.values)) {
captchaUrl = this.buildCaptchaUrl();
this.captchaModeSubject$.next(recaptchaModeRD.payload?.values[0]);
}
break;
default:
// TODO handle error
}
if (captchaUrl) {
this.loadScript(captchaUrl);
}
}
});
}
/**
* Returns an observable of string
* @param action action is the process type in which used to protect multiple spam REST calls
*/
public getRecaptchaToken(action) {
return this.captchaKey().pipe(
switchMap((key) => grecaptcha.execute(key, {action: action}))
);
}
/**
* Returns an observable of string
*/
public executeRecaptcha() {
return of(grecaptcha.execute());
}
public getRecaptchaTokenResponse() {
return grecaptcha.getResponse();
}
/**
* Return the google captcha ur with google captchas api key
*
* @param key contains a secret key of a google captchas
* @returns string which has google captcha url with google captchas key
*/
buildCaptchaUrl(key?: string) {
const apiUrl = 'https://www.google.com/recaptcha/api.js';
return key ? `${apiUrl}?render=${key}` : apiUrl;
}
/**
* Append the google captchas script to the document
*
* @param url contains a script url which will be loaded into page
* @returns A promise
*/
private loadScript(url) {
return new Promise((resolve, reject) => {
const script = this.renderer.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.text = ``;
script.onload = resolve;
script.onerror = reject;
this.renderer.appendChild(this._document.head, script);
});
}
refreshCaptchaScript = () => {
this.loadRecaptchaProperties();
};
}

View File

@@ -25,5 +25,12 @@ export class Registration implements UnCacheableObject {
* The token linked to the registration * The token linked to the registration
*/ */
token: string; token: string;
/**
* The token linked to the registration
*/
groupNames: string[];
/**
* The token linked to the registration
*/
groups: string[];
} }

View File

@@ -2,7 +2,7 @@
<h2>{{MESSAGE_PREFIX + '.header'|translate}}</h2> <h2>{{MESSAGE_PREFIX + '.header'|translate}}</h2>
<p>{{MESSAGE_PREFIX + '.info' | translate}}</p> <p>{{MESSAGE_PREFIX + '.info' | translate}}</p>
<form [class]="'ng-invalid'" [formGroup]="form" (ngSubmit)="register()"> <form [class]="'ng-invalid'" [formGroup]="form">
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
@@ -28,9 +28,30 @@
</div> </div>
</div> </div>
<ds-alert [type]="AlertTypeEnum.Warning" *ngIf="registrationVerification && !isRecaptchaCookieAccepted()">
<p class="m-0" [innerHTML]="MESSAGE_PREFIX + '.google-recaptcha.must-accept-cookies' | translate"></p>
<p class="m-0"><a href="javascript:void(0);" (click)="this.klaroService.showSettings()">{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}</a></p>
</ds-alert>
<div class="my-3" *ngIf="isRecaptchaCookieAccepted() && (googleRecaptchaService.captchaVersion() | async) === 'v2'">
<ds-google-recaptcha [captchaMode]="(googleRecaptchaService.captchaMode() | async)"
(executeRecaptcha)="register($event)" (checkboxChecked)="onCheckboxChecked($event)"
(showNotification)="showNotification($event)"></ds-google-recaptcha>
</div>
<ng-container *ngIf="!((googleRecaptchaService.captchaVersion() | async) === 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible">
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
{{ MESSAGE_PREFIX + '.submit' | translate }}
</button>
</ng-container>
<ng-template #v2Invisible>
<button class="btn btn-primary" [disabled]="form.invalid" (click)="executeRecaptcha()">
{{ MESSAGE_PREFIX + '.submit' | translate }}
</button>
</ng-template>
</form> </form>
<button class="btn btn-primary"
[disabled]="form.invalid"
(click)="register()">{{MESSAGE_PREFIX + '.submit'| translate}}</button>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { of as observableOf } from 'rxjs'; import { of as observableOf, of } from 'rxjs';
import { RestResponse } from '../core/cache/response.models'; import { RestResponse } from '../core/cache/response.models';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
@@ -14,6 +14,10 @@ import { RouterStub } from '../shared/testing/router.stub';
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
import { RegisterEmailFormComponent } from './register-email-form.component'; import { RegisterEmailFormComponent } from './register-email-form.component';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service';
import { CookieService } from '../core/services/cookie.service';
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
describe('RegisterEmailComponent', () => { describe('RegisterEmailComponent', () => {
@@ -24,6 +28,22 @@ describe('RegisterEmailComponent', () => {
let epersonRegistrationService: EpersonRegistrationService; let epersonRegistrationService: EpersonRegistrationService;
let notificationsService; let notificationsService;
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: jasmine.createSpy('findByPropertyName')
});
const captchaVersion$ = of('v3');
const captchaMode$ = of('invisible');
const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['true'] });
const confResponseDisabled$ = createSuccessfulRemoteDataObject$({ values: ['false'] });
const googleRecaptchaService = jasmine.createSpyObj('googleRecaptchaService', {
getRecaptchaToken: Promise.resolve('googleRecaptchaToken'),
executeRecaptcha: Promise.resolve('googleRecaptchaToken'),
getRecaptchaTokenResponse: Promise.resolve('googleRecaptchaToken'),
captchaVersion: captchaVersion$,
captchaMode: captchaMode$,
});
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
router = new RouterStub(); router = new RouterStub();
@@ -39,8 +59,11 @@ describe('RegisterEmailComponent', () => {
providers: [ providers: [
{provide: Router, useValue: router}, {provide: Router, useValue: router},
{provide: EpersonRegistrationService, useValue: epersonRegistrationService}, {provide: EpersonRegistrationService, useValue: epersonRegistrationService},
{provide: ConfigurationDataService, useValue: configurationDataService},
{provide: FormBuilder, useValue: new FormBuilder()}, {provide: FormBuilder, useValue: new FormBuilder()},
{provide: NotificationsService, useValue: notificationsService}, {provide: NotificationsService, useValue: notificationsService},
{provide: CookieService, useValue: new CookieServiceMock()},
{provide: GoogleRecaptchaService, useValue: googleRecaptchaService},
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -48,6 +71,9 @@ describe('RegisterEmailComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(RegisterEmailFormComponent); fixture = TestBed.createComponent(RegisterEmailFormComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
googleRecaptchaService.captchaVersion$ = captchaVersion$;
googleRecaptchaService.captchaMode$ = captchaMode$;
configurationDataService.findByPropertyName.and.returnValues(confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$);
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -90,4 +116,33 @@ describe('RegisterEmailComponent', () => {
expect(router.navigate).not.toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled();
}); });
}); });
describe('register with google recaptcha', () => {
beforeEach(fakeAsync(() => {
configurationDataService.findByPropertyName.and.returnValues(confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$);
googleRecaptchaService.captchaVersion$ = captchaVersion$;
googleRecaptchaService.captchaMode$ = captchaMode$;
comp.ngOnInit();
fixture.detectChanges();
}));
it('should send a registration to the service and on success display a message and return to home', fakeAsync(() => {
comp.form.patchValue({email: 'valid@email.org'});
comp.register();
tick();
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken');
expect(notificationsService.success).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(['/home']);
}));
it('should send a registration to the service and on error display a message', fakeAsync(() => {
(epersonRegistrationService.registerEmail as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 400, 'Bad Request')));
comp.form.patchValue({email: 'valid@email.org'});
comp.register();
tick();
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken');
expect(notificationsService.error).toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
}));
});
}); });

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnInit, Optional } from '@angular/core';
import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
import { NotificationsService } from '../shared/notifications/notifications.service'; import { NotificationsService } from '../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -6,6 +6,16 @@ import { Router } from '@angular/router';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Registration } from '../core/shared/registration.model'; import { Registration } from '../core/shared/registration.model';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { isNotEmpty } from '../shared/empty.util';
import { BehaviorSubject, combineLatest, Observable, of, switchMap } from 'rxjs';
import { map, startWith, take } from 'rxjs/operators';
import { CAPTCHA_NAME, GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service';
import { AlertType } from '../shared/alert/aletr-type';
import { KlaroService } from '../shared/cookies/klaro.service';
import { CookieService } from '../core/services/cookie.service';
@Component({ @Component({
selector: 'ds-register-email-form', selector: 'ds-register-email-form',
@@ -27,12 +37,40 @@ export class RegisterEmailFormComponent implements OnInit {
@Input() @Input()
MESSAGE_PREFIX: string; MESSAGE_PREFIX: string;
public AlertTypeEnum = AlertType;
/**
* registration verification configuration
*/
registrationVerification = false;
/**
* Return true if the user completed the reCaptcha verification (checkbox mode)
*/
checkboxCheckedSubject$ = new BehaviorSubject<boolean>(false);
disableUntilChecked = true;
captchaVersion(): Observable<string> {
return this.googleRecaptchaService.captchaVersion();
}
captchaMode(): Observable<string> {
return this.googleRecaptchaService.captchaMode();
}
constructor( constructor(
private epersonRegistrationService: EpersonRegistrationService, private epersonRegistrationService: EpersonRegistrationService,
private notificationService: NotificationsService, private notificationService: NotificationsService,
private translateService: TranslateService, private translateService: TranslateService,
private router: Router, private router: Router,
private formBuilder: FormBuilder private formBuilder: FormBuilder,
private configService: ConfigurationDataService,
public googleRecaptchaService: GoogleRecaptchaService,
public cookieService: CookieService,
@Optional() public klaroService: KlaroService,
private changeDetectorRef: ChangeDetectorRef,
private notificationsService: NotificationsService,
) { ) {
} }
@@ -45,15 +83,70 @@ export class RegisterEmailFormComponent implements OnInit {
], ],
}) })
}); });
this.configService.findByPropertyName('registration.verification.enabled').pipe(
getFirstSucceededRemoteDataPayload(),
map((res: ConfigurationProperty) => res?.values[0].toLowerCase() === 'true')
).subscribe((res: boolean) => {
this.registrationVerification = res;
});
this.disableUntilCheckedFcn().subscribe((res) => {
this.disableUntilChecked = res;
this.changeDetectorRef.detectChanges();
});
}
/**
* execute the captcha function for v2 invisible
*/
executeRecaptcha() {
this.googleRecaptchaService.executeRecaptcha();
} }
/** /**
* Register an email address * Register an email address
*/ */
register() { register(tokenV2?) {
if (!this.form.invalid) { if (!this.form.invalid) {
this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RemoteData<Registration>) => { if (this.registrationVerification) {
combineLatest([this.captchaVersion(), this.captchaMode()]).pipe(
switchMap(([captchaVersion, captchaMode]) => {
if (captchaVersion === 'v3') {
return this.googleRecaptchaService.getRecaptchaToken('register_email');
} else if (captchaVersion === 'v2' && captchaMode === 'checkbox') {
return of(this.googleRecaptchaService.getRecaptchaTokenResponse());
} else if (captchaVersion === 'v2' && captchaMode === 'invisible') {
return of(tokenV2);
} else {
console.error(`Invalid reCaptcha configuration: version = ${captchaVersion}, mode = ${captchaMode}`);
this.showNotification('error');
}
}),
take(1),
).subscribe((token) => {
if (isNotEmpty(token)) {
this.registration(token);
} else {
console.error('reCaptcha error');
this.showNotification('error');
}
}
);
} else {
this.registration();
}
}
}
/**
* Registration of an email address
*/
registration(captchaToken = null) {
let registerEmail$ = captchaToken ?
this.epersonRegistrationService.registerEmail(this.email.value, captchaToken) :
this.epersonRegistrationService.registerEmail(this.email.value);
registerEmail$.subscribe((response: RemoteData<Registration>) => {
if (response.hasSucceeded) { if (response.hasSucceeded) {
this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`), this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`),
this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value})); this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value}));
@@ -62,13 +155,55 @@ export class RegisterEmailFormComponent implements OnInit {
this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`),
this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value})); this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value}));
} }
});
} }
/**
* Return true if the user has accepted the required cookies for reCaptcha
*/
isRecaptchaCookieAccepted(): boolean {
const klaroAnonymousCookie = this.cookieService.get('klaro-anonymous');
return isNotEmpty(klaroAnonymousCookie) ? klaroAnonymousCookie[CAPTCHA_NAME] : false;
}
/**
* Return true if the user has not completed the reCaptcha verification (checkbox mode)
*/
disableUntilCheckedFcn(): Observable<boolean> {
const checked$ = this.checkboxCheckedSubject$.asObservable();
return combineLatest([this.captchaVersion(), this.captchaMode(), checked$]).pipe(
// disable if checkbox is not checked or if reCaptcha is not in v2 checkbox mode
switchMap(([captchaVersion, captchaMode, checked]) => captchaVersion === 'v2' && captchaMode === 'checkbox' ? of(!checked) : of(false)),
startWith(true),
); );
} }
}
get email() { get email() {
return this.form.get('email'); return this.form.get('email');
} }
onCheckboxChecked(checked: boolean) {
this.checkboxCheckedSubject$.next(checked);
}
/**
* Show a notification to the user
* @param key
*/
showNotification(key) {
const notificationTitle = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.title');
const notificationErrorMsg = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.message.error');
const notificationExpiredMsg = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.message.expired');
switch (key) {
case 'expired':
this.notificationsService.warning(notificationTitle, notificationExpiredMsg);
break;
case 'error':
this.notificationsService.error(notificationTitle, notificationErrorMsg);
break;
default:
console.warn(`Unimplemented notification '${key}' from reCaptcha service`);
}
}
} }

View File

@@ -6,7 +6,7 @@ import { RegisterEmailFormComponent } from './register-email-form.component';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule
], ],
declarations: [ declarations: [
RegisterEmailFormComponent, RegisterEmailFormComponent,

View File

@@ -21,6 +21,8 @@ describe('BrowserKlaroService', () => {
const trackingIdProp = 'google.analytics.key'; const trackingIdProp = 'google.analytics.key';
const trackingIdTestValue = 'mock-tracking-id'; const trackingIdTestValue = 'mock-tracking-id';
const googleAnalytics = 'google-analytics'; const googleAnalytics = 'google-analytics';
const recaptchaProp = 'registration.verification.enabled';
const recaptchaValue = 'true';
let translateService; let translateService;
let ePersonService; let ePersonService;
let authService; let authService;
@@ -31,8 +33,8 @@ describe('BrowserKlaroService', () => {
let configurationDataService: ConfigurationDataService; let configurationDataService: ConfigurationDataService;
const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', { const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$({ findByPropertyName: createSuccessfulRemoteDataObject$({
...new ConfigurationProperty(), ... new ConfigurationProperty(),
name: trackingIdProp, name: recaptchaProp,
values: values, values: values,
}), }),
}); });
@@ -57,7 +59,7 @@ describe('BrowserKlaroService', () => {
isAuthenticated: observableOf(true), isAuthenticated: observableOf(true),
getAuthenticatedUserFromStore: observableOf(user) getAuthenticatedUserFromStore: observableOf(user)
}); });
configurationDataService = createConfigSuccessSpy(trackingIdTestValue); configurationDataService = createConfigSuccessSpy(recaptchaValue);
findByPropertyName = configurationDataService.findByPropertyName; findByPropertyName = configurationDataService.findByPropertyName;
cookieService = jasmine.createSpyObj('cookieService', { cookieService = jasmine.createSpyObj('cookieService', {
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}', get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
@@ -298,15 +300,18 @@ describe('BrowserKlaroService', () => {
describe('initialize google analytics configuration', () => { describe('initialize google analytics configuration', () => {
let GOOGLE_ANALYTICS_KEY; let GOOGLE_ANALYTICS_KEY;
let REGISTRATION_VERIFICATION_ENABLED_KEY;
beforeEach(() => { beforeEach(() => {
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY); GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
configurationDataService.findByPropertyName = findByPropertyName; REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY);
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user)); spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
translateService.get.and.returnValue(observableOf('loading...')); translateService.get.and.returnValue(observableOf('loading...'));
spyOn(service, 'addAppMessages'); spyOn(service, 'addAppMessages');
spyOn((service as any), 'initializeUser'); spyOn((service as any), 'initializeUser');
spyOn(service, 'translateConfiguration'); spyOn(service, 'translateConfiguration');
configurationDataService.findByPropertyName = findByPropertyName;
}); });
it('should not filter googleAnalytics when servicesToHide are empty', () => { it('should not filter googleAnalytics when servicesToHide are empty', () => {
const filteredConfig = (service as any).filterConfigServices([]); const filteredConfig = (service as any).filterConfigServices([]);
expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics })); expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics }));
@@ -316,30 +321,74 @@ describe('BrowserKlaroService', () => {
expect(filteredConfig).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); expect(filteredConfig).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
}); });
it('should have been initialized with googleAnalytics', () => { it('should have been initialized with googleAnalytics', () => {
configurationDataService.findByPropertyName = jasmine.createSpy('configurationDataService').and.returnValue(
createSuccessfulRemoteDataObject$({
...new ConfigurationProperty(),
name: trackingIdProp,
values: [googleAnalytics],
})
);
service.initialize(); service.initialize();
expect(service.klaroConfig.services).toContain(jasmine.objectContaining({ name: googleAnalytics })); expect(service.klaroConfig.services).toContain(jasmine.objectContaining({ name: googleAnalytics }));
}); });
it('should filter googleAnalytics when empty configuration is retrieved', () => { it('should filter googleAnalytics when empty configuration is retrieved', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue( configurationDataService.findByPropertyName =
jasmine.createSpy()
.withArgs(GOOGLE_ANALYTICS_KEY)
.and
.returnValue(
createSuccessfulRemoteDataObject$({ createSuccessfulRemoteDataObject$({
...new ConfigurationProperty(), ... new ConfigurationProperty(),
name: googleAnalytics, name: googleAnalytics,
values: [], values: [],
})); }
)
)
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
.and
.returnValue(
createSuccessfulRemoteDataObject$({
... new ConfigurationProperty(),
name: trackingIdTestValue,
values: ['false'],
})
);
service.initialize(); service.initialize();
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
}); });
it('should filter googleAnalytics when an error occurs', () => { it('should filter googleAnalytics when an error occurs', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue( configurationDataService.findByPropertyName =
createFailedRemoteDataObject$('Erro while loading GA') jasmine.createSpy()
.withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
createFailedRemoteDataObject$('Error while loading GA')
)
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
.and
.returnValue(
createSuccessfulRemoteDataObject$({
... new ConfigurationProperty(),
name: trackingIdTestValue,
values: ['false'],
})
); );
service.initialize(); service.initialize();
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
}); });
it('should filter googleAnalytics when an invalid payload is retrieved', () => { it('should filter googleAnalytics when an invalid payload is retrieved', () => {
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue( configurationDataService.findByPropertyName =
jasmine.createSpy()
.withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
createSuccessfulRemoteDataObject$(null) createSuccessfulRemoteDataObject$(null)
)
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
.and
.returnValue(
createSuccessfulRemoteDataObject$({
... new ConfigurationProperty(),
name: trackingIdTestValue,
values: ['false'],
})
); );
service.initialize(); service.initialize();
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));

View File

@@ -15,6 +15,7 @@ import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-config
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
/** /**
* Metadata field to store a user's cookie consent preferences in * Metadata field to store a user's cookie consent preferences in
@@ -49,6 +50,8 @@ export class BrowserKlaroService extends KlaroService {
private readonly GOOGLE_ANALYTICS_KEY = 'google.analytics.key'; private readonly GOOGLE_ANALYTICS_KEY = 'google.analytics.key';
private readonly REGISTRATION_VERIFICATION_ENABLED_KEY = 'registration.verification.enabled';
private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics'; private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics';
/** /**
@@ -78,15 +81,30 @@ export class BrowserKlaroService extends KlaroService {
this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
} }
const servicesToHide$: Observable<string[]> = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe( const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map(remoteData => { map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)),
if (!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)) { );
return [this.GOOGLE_ANALYTICS_SERVICE_NAME];
} else { const hideRegistrationVerification$ = this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe(
return []; getFirstCompletedRemoteData(),
map((remoteData) =>
!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() !== 'true'
),
);
const servicesToHide$: Observable<string[]> = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$]).pipe(
map(([hideGoogleAnalytics, hideRegistrationVerification]) => {
let servicesToHideArray: string[] = [];
if (hideGoogleAnalytics) {
servicesToHideArray.push(this.GOOGLE_ANALYTICS_SERVICE_NAME);
} }
}), if (hideRegistrationVerification) {
servicesToHideArray.push(CAPTCHA_NAME);
}
console.log(servicesToHideArray);
return servicesToHideArray;
})
); );
this.translateService.setDefaultLang(environment.defaultLanguage); this.translateService.setDefaultLang(environment.defaultLanguage);
@@ -308,4 +326,5 @@ export class BrowserKlaroService extends KlaroService {
private filterConfigServices(servicesToHide: string[]): Pick<typeof klaroConfiguration, 'services'>[] { private filterConfigServices(servicesToHide: string[]): Pick<typeof klaroConfiguration, 'services'>[] {
return this.klaroConfig.services.filter(service => !servicesToHide.some(name => name === service.name)); return this.klaroConfig.services.filter(service => !servicesToHide.some(name => name === service.name));
} }
} }

View File

@@ -1,6 +1,7 @@
import { TOKENITEM } from '../../core/auth/models/auth-token-info.model'; import { TOKENITEM } from '../../core/auth/models/auth-token-info.model';
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service'; import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service';
import { LANG_COOKIE } from '../../core/locale/locale.service'; import { LANG_COOKIE } from '../../core/locale/locale.service';
import { CAPTCHA_COOKIE, CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
/** /**
* Cookie for has_agreed_end_user * Cookie for has_agreed_end_user
@@ -157,5 +158,17 @@ export const klaroConfiguration: any = {
*/ */
onlyOnce: true, onlyOnce: true,
}, },
{
name: CAPTCHA_NAME,
purposes: ['registration-password-recovery'],
required: false,
cookies: [
[/^klaro-.+$/],
CAPTCHA_COOKIE
],
onAccept: `window.refreshCaptchaScript?.call()`,
onDecline: `window.refreshCaptchaScript?.call()`,
onlyOnce: true,
}
], ],
}; };

View File

@@ -0,0 +1,6 @@
<div class="g-recaptcha"
[attr.data-callback]="'dataCallback'"
[attr.data-expired-callback]="'expiredCallback'"
[attr.data-error-callback]="'errorCallback'"
[attr.data-sitekey]="(recaptchaKey$ | async)?.values[0]"
[attr.data-size]="captchaMode === 'invisible' ? 'invisible' : null"></div>

View File

@@ -0,0 +1,50 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NativeWindowService } from '../../core/services/window.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { NativeWindowMockFactory } from '../mocks/mock-native-window-ref';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { GoogleRecaptchaComponent } from './google-recaptcha.component';
describe('GoogleRecaptchaComponent', () => {
let component: GoogleRecaptchaComponent;
let fixture: ComponentFixture<GoogleRecaptchaComponent>;
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: jasmine.createSpy('findByPropertyName')
});
const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['valid-google-recaptcha-key'] });
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ GoogleRecaptchaComponent ],
providers: [
{ provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(GoogleRecaptchaComponent);
component = fixture.componentInstance;
configurationDataService.findByPropertyName.and.returnValues(confResponse$);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should rendered google recaptcha.',() => {
const container = fixture.debugElement.query(By.css('.g-recaptcha'));
expect(container).toBeTruthy();
});
});

View File

@@ -0,0 +1,70 @@
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { Observable } from 'rxjs';
import { NativeWindowRef, NativeWindowService } from 'src/app/core/services/window.service';
import { isNotEmpty } from '../empty.util';
@Component({
selector: 'ds-google-recaptcha',
templateUrl: './google-recaptcha.component.html',
styleUrls: ['./google-recaptcha.component.scss'],
})
export class GoogleRecaptchaComponent implements OnInit {
@Input() captchaMode: string;
/**
* An EventEmitter that's fired whenever the form is being submitted
*/
@Output() executeRecaptcha: EventEmitter<any> = new EventEmitter();
@Output() checkboxChecked: EventEmitter<boolean> = new EventEmitter();
@Output() showNotification: EventEmitter<any> = new EventEmitter();
recaptchaKey$: Observable<any>;
constructor(
@Inject(NativeWindowService) private _window: NativeWindowRef,
private configService: ConfigurationDataService,
) {
}
/**
* Retrieve the google recaptcha site key
*/
ngOnInit() {
this.recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe(
getFirstSucceededRemoteDataPayload(),
);
this._window.nativeWindow.dataCallback = this.dataCallbackFcn;
this._window.nativeWindow.expiredCallback = this.expiredCallbackFcn;
this._window.nativeWindow.errorCallback = this.errorCallbackFcn;
}
dataCallbackFcn = ($event) => {
switch (this.captchaMode) {
case 'invisible':
this.executeRecaptcha.emit($event);
break;
case 'checkbox':
this.checkboxChecked.emit(isNotEmpty($event));
break;
default:
console.error(`Invalid reCaptcha mode '${this.captchaMode}`);
this.showNotification.emit('error');
}
};
expiredCallbackFcn = () => {
this.checkboxChecked.emit(false);
this.showNotification.emit('expired');
};
errorCallbackFcn = () => {
this.showNotification.emit('error');
};
}

View File

@@ -323,6 +323,7 @@ import {
ItemPageTitleFieldComponent ItemPageTitleFieldComponent
} from '../item-page/simple/field-components/specific-field/title/item-page-title-field.component'; } from '../item-page/simple/field-components/specific-field/title/item-page-title-field.component';
import { MarkdownPipe } from './utils/markdown.pipe'; import { MarkdownPipe } from './utils/markdown.pipe';
import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module';
const MODULES = [ const MODULES = [
CommonModule, CommonModule,
@@ -343,7 +344,8 @@ const MODULES = [
NouisliderModule, NouisliderModule,
MomentModule, MomentModule,
DragDropModule, DragDropModule,
CdkTreeModule CdkTreeModule,
GoogleRecaptchaModule,
]; ];
const ROOT_MODULES = [ const ROOT_MODULES = [

View File

@@ -1279,10 +1279,20 @@
"cookies.consent.app.title.google-recaptcha": "Google reCaptcha",
"cookies.consent.app.description.google-recaptcha": "We use google reCAPTCHA service during registration and password recovery",
"cookies.consent.purpose.functional": "Functional", "cookies.consent.purpose.functional": "Functional",
"cookies.consent.purpose.statistical": "Statistical", "cookies.consent.purpose.statistical": "Statistical",
"cookies.consent.purpose.registration-password-recovery": "Registration and Password recovery",
"cookies.consent.purpose.sharing": "Sharing",
"curation-task.task.citationpage.label": "Generate Citation Page", "curation-task.task.citationpage.label": "Generate Citation Page",
"curation-task.task.checklinks.label": "Check Links in Metadata", "curation-task.task.checklinks.label": "Check Links in Metadata",
@@ -3248,7 +3258,17 @@
"register-page.registration.error.content": "An error occured when registering the following email address: {{ email }}", "register-page.registration.error.content": "An error occured when registering the following email address: {{ email }}",
"register-page.registration.error.recaptcha": "Error when trying to authenticate with recaptcha",
"register-page.registration.google-recaptcha.must-accept-cookies": "In order to register you must accept the <b>Registration and Password recovery</b> (Google reCaptcha) cookies.",
"register-page.registration.google-recaptcha.open-cookie-settings": "Open cookie settings",
"register-page.registration.google-recaptcha.notification.title": "Google reCaptcha",
"register-page.registration.google-recaptcha.notification.message.error": "An error occurred during reCaptcha verification",
"register-page.registration.google-recaptcha.notification.message.expired": "Verification expired. Please verify again.",
"relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items", "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items",

View File

@@ -2,7 +2,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [] "types": ["grecaptcha"]
}, },
"files": [ "files": [
"src/main.browser.ts", "src/main.browser.ts",

View File

@@ -4,7 +4,8 @@
"outDir": "./out-tsc/app-server", "outDir": "./out-tsc/app-server",
"target": "es2016", "target": "es2016",
"types": [ "types": [
"node" "node",
"grecaptcha"
] ]
}, },
"files": [ "files": [

View File

@@ -5,7 +5,8 @@
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": [
"jasmine", "jasmine",
"node" "node",
"grecaptcha"
] ]
}, },
"files": [ "files": [

View File

@@ -2247,6 +2247,11 @@
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7" resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7"
integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ== integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==
"@types/grecaptcha@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/grecaptcha/-/grecaptcha-3.0.4.tgz#3de601f3b0cd0298faf052dd5bd62aff64c2be2e"
integrity sha512-7l1Y8DTGXkx/r4pwU1nMVAR+yD/QC+MCHKXAyEX/7JZhwcN1IED09aZ9vCjjkcGdhSQiu/eJqcXInpl6eEEEwg==
"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"