mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
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:
@@ -24,7 +24,7 @@ import 'cypress-axe';
|
||||
beforeEach(() => {
|
||||
// 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.
|
||||
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.
|
||||
|
@@ -78,6 +78,7 @@
|
||||
"@nguniversal/express-engine": "^13.0.2",
|
||||
"@ngx-translate/core": "^13.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
"angulartics2": "^12.0.0",
|
||||
"axios": "^0.27.2",
|
||||
|
@@ -9,6 +9,8 @@ import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RequestEntry } from './request-entry.model';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
|
||||
describe('EpersonRegistrationService', () => {
|
||||
let testScheduler;
|
||||
@@ -79,8 +81,23 @@ describe('EpersonRegistrationService', () => {
|
||||
it('should send an email registration', () => {
|
||||
|
||||
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 }));
|
||||
});
|
||||
});
|
||||
|
@@ -3,15 +3,17 @@ import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { GetRequest, PostRequest } from './request.models';
|
||||
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 { Registration } from '../shared/registration.model';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { RegistrationResponseParsingService } from './registration-response-parsing.service';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -51,8 +53,9 @@ export class EpersonRegistrationService {
|
||||
/**
|
||||
* Register a new email address
|
||||
* @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();
|
||||
registration.email = email;
|
||||
|
||||
@@ -60,10 +63,17 @@ export class EpersonRegistrationService {
|
||||
|
||||
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(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
const request = new PostRequest(requestId, href, registration);
|
||||
const request = new PostRequest(requestId, href, registration, options);
|
||||
this.requestService.send(request);
|
||||
})
|
||||
).subscribe();
|
||||
|
25
src/app/core/google-recaptcha/google-recaptcha.module.ts
Normal file
25
src/app/core/google-recaptcha/google-recaptcha.module.ts
Normal 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 {}
|
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
176
src/app/core/google-recaptcha/google-recaptcha.service.ts
Normal file
176
src/app/core/google-recaptcha/google-recaptcha.service.ts
Normal 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();
|
||||
};
|
||||
|
||||
}
|
@@ -25,5 +25,12 @@ export class Registration implements UnCacheableObject {
|
||||
* The token linked to the registration
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* The token linked to the registration
|
||||
*/
|
||||
groupNames: string[];
|
||||
/**
|
||||
* The token linked to the registration
|
||||
*/
|
||||
groups: string[];
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<h2>{{MESSAGE_PREFIX + '.header'|translate}}</h2>
|
||||
<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="row">
|
||||
@@ -28,9 +28,30 @@
|
||||
</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>
|
||||
|
||||
<button class="btn btn-primary"
|
||||
[disabled]="form.invalid"
|
||||
(click)="register()">{{MESSAGE_PREFIX + '.submit'| translate}}</button>
|
||||
|
||||
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { waitForAsync, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { of as observableOf, of } from 'rxjs';
|
||||
import { RestResponse } from '../core/cache/response.models';
|
||||
import { CommonModule } from '@angular/common';
|
||||
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 { RegisterEmailFormComponent } from './register-email-form.component';
|
||||
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', () => {
|
||||
|
||||
@@ -24,6 +28,22 @@ describe('RegisterEmailComponent', () => {
|
||||
let epersonRegistrationService: EpersonRegistrationService;
|
||||
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(() => {
|
||||
|
||||
router = new RouterStub();
|
||||
@@ -39,8 +59,11 @@ describe('RegisterEmailComponent', () => {
|
||||
providers: [
|
||||
{provide: Router, useValue: router},
|
||||
{provide: EpersonRegistrationService, useValue: epersonRegistrationService},
|
||||
{provide: ConfigurationDataService, useValue: configurationDataService},
|
||||
{provide: FormBuilder, useValue: new FormBuilder()},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: CookieService, useValue: new CookieServiceMock()},
|
||||
{provide: GoogleRecaptchaService, useValue: googleRecaptchaService},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -48,6 +71,9 @@ describe('RegisterEmailComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RegisterEmailFormComponent);
|
||||
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();
|
||||
});
|
||||
@@ -90,4 +116,33 @@ describe('RegisterEmailComponent', () => {
|
||||
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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -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 { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@@ -6,6 +6,16 @@ import { Router } from '@angular/router';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { Registration } from '../core/shared/registration.model';
|
||||
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({
|
||||
selector: 'ds-register-email-form',
|
||||
@@ -27,12 +37,40 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
@Input()
|
||||
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(
|
||||
private epersonRegistrationService: EpersonRegistrationService,
|
||||
private notificationService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
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,30 +83,127 @@ 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() {
|
||||
register(tokenV2?) {
|
||||
if (!this.form.invalid) {
|
||||
this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RemoteData<Registration>) => {
|
||||
if (response.hasSucceeded) {
|
||||
this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`),
|
||||
this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value}));
|
||||
this.router.navigate(['/home']);
|
||||
} else {
|
||||
this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`),
|
||||
this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value}));
|
||||
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) {
|
||||
this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`),
|
||||
this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value}));
|
||||
this.router.navigate(['/home']);
|
||||
} else {
|
||||
this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`),
|
||||
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() {
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { RegisterEmailFormComponent } from './register-email-form.component';
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
RegisterEmailFormComponent,
|
||||
|
@@ -21,6 +21,8 @@ describe('BrowserKlaroService', () => {
|
||||
const trackingIdProp = 'google.analytics.key';
|
||||
const trackingIdTestValue = 'mock-tracking-id';
|
||||
const googleAnalytics = 'google-analytics';
|
||||
const recaptchaProp = 'registration.verification.enabled';
|
||||
const recaptchaValue = 'true';
|
||||
let translateService;
|
||||
let ePersonService;
|
||||
let authService;
|
||||
@@ -31,8 +33,8 @@ describe('BrowserKlaroService', () => {
|
||||
let configurationDataService: ConfigurationDataService;
|
||||
const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$({
|
||||
...new ConfigurationProperty(),
|
||||
name: trackingIdProp,
|
||||
... new ConfigurationProperty(),
|
||||
name: recaptchaProp,
|
||||
values: values,
|
||||
}),
|
||||
});
|
||||
@@ -57,7 +59,7 @@ describe('BrowserKlaroService', () => {
|
||||
isAuthenticated: observableOf(true),
|
||||
getAuthenticatedUserFromStore: observableOf(user)
|
||||
});
|
||||
configurationDataService = createConfigSuccessSpy(trackingIdTestValue);
|
||||
configurationDataService = createConfigSuccessSpy(recaptchaValue);
|
||||
findByPropertyName = configurationDataService.findByPropertyName;
|
||||
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}',
|
||||
@@ -298,15 +300,18 @@ describe('BrowserKlaroService', () => {
|
||||
|
||||
describe('initialize google analytics configuration', () => {
|
||||
let GOOGLE_ANALYTICS_KEY;
|
||||
let REGISTRATION_VERIFICATION_ENABLED_KEY;
|
||||
beforeEach(() => {
|
||||
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));
|
||||
translateService.get.and.returnValue(observableOf('loading...'));
|
||||
spyOn(service, 'addAppMessages');
|
||||
spyOn((service as any), 'initializeUser');
|
||||
spyOn(service, 'translateConfiguration');
|
||||
configurationDataService.findByPropertyName = findByPropertyName;
|
||||
});
|
||||
|
||||
it('should not filter googleAnalytics when servicesToHide are empty', () => {
|
||||
const filteredConfig = (service as any).filterConfigServices([]);
|
||||
expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
@@ -316,31 +321,75 @@ describe('BrowserKlaroService', () => {
|
||||
expect(filteredConfig).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
it('should have been initialized with googleAnalytics', () => {
|
||||
configurationDataService.findByPropertyName = jasmine.createSpy('configurationDataService').and.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
...new ConfigurationProperty(),
|
||||
name: trackingIdProp,
|
||||
values: [googleAnalytics],
|
||||
})
|
||||
);
|
||||
service.initialize();
|
||||
expect(service.klaroConfig.services).toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
it('should filter googleAnalytics when empty configuration is retrieved', () => {
|
||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
...new ConfigurationProperty(),
|
||||
name: googleAnalytics,
|
||||
values: [],
|
||||
}));
|
||||
configurationDataService.findByPropertyName =
|
||||
jasmine.createSpy()
|
||||
.withArgs(GOOGLE_ANALYTICS_KEY)
|
||||
.and
|
||||
.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
... new ConfigurationProperty(),
|
||||
name: googleAnalytics,
|
||||
values: [],
|
||||
}
|
||||
)
|
||||
)
|
||||
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
|
||||
.and
|
||||
.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
... new ConfigurationProperty(),
|
||||
name: trackingIdTestValue,
|
||||
values: ['false'],
|
||||
})
|
||||
);
|
||||
|
||||
service.initialize();
|
||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
it('should filter googleAnalytics when an error occurs', () => {
|
||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
createFailedRemoteDataObject$('Erro while loading GA')
|
||||
);
|
||||
configurationDataService.findByPropertyName =
|
||||
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();
|
||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
it('should filter googleAnalytics when an invalid payload is retrieved', () => {
|
||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
createSuccessfulRemoteDataObject$(null)
|
||||
);
|
||||
configurationDataService.findByPropertyName =
|
||||
jasmine.createSpy()
|
||||
.withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
createSuccessfulRemoteDataObject$(null)
|
||||
)
|
||||
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
|
||||
.and
|
||||
.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
... new ConfigurationProperty(),
|
||||
name: trackingIdTestValue,
|
||||
values: ['false'],
|
||||
})
|
||||
);
|
||||
service.initialize();
|
||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
|
@@ -15,6 +15,7 @@ import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-config
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
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
|
||||
@@ -49,6 +50,8 @@ export class BrowserKlaroService extends KlaroService {
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -78,15 +81,30 @@ export class BrowserKlaroService extends KlaroService {
|
||||
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(),
|
||||
map(remoteData => {
|
||||
if (!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)) {
|
||||
return [this.GOOGLE_ANALYTICS_SERVICE_NAME];
|
||||
} else {
|
||||
return [];
|
||||
map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)),
|
||||
);
|
||||
|
||||
const hideRegistrationVerification$ = this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe(
|
||||
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);
|
||||
@@ -308,4 +326,5 @@ export class BrowserKlaroService extends KlaroService {
|
||||
private filterConfigServices(servicesToHide: string[]): Pick<typeof klaroConfiguration, 'services'>[] {
|
||||
return this.klaroConfig.services.filter(service => !servicesToHide.some(name => name === service.name));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { TOKENITEM } from '../../core/auth/models/auth-token-info.model';
|
||||
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.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
|
||||
@@ -157,5 +158,17 @@ export const klaroConfiguration: any = {
|
||||
*/
|
||||
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,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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');
|
||||
};
|
||||
|
||||
}
|
@@ -323,6 +323,7 @@ import {
|
||||
ItemPageTitleFieldComponent
|
||||
} from '../item-page/simple/field-components/specific-field/title/item-page-title-field.component';
|
||||
import { MarkdownPipe } from './utils/markdown.pipe';
|
||||
import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module';
|
||||
|
||||
const MODULES = [
|
||||
CommonModule,
|
||||
@@ -343,7 +344,8 @@ const MODULES = [
|
||||
NouisliderModule,
|
||||
MomentModule,
|
||||
DragDropModule,
|
||||
CdkTreeModule
|
||||
CdkTreeModule,
|
||||
GoogleRecaptchaModule,
|
||||
];
|
||||
|
||||
const ROOT_MODULES = [
|
||||
|
@@ -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.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.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.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",
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
"types": ["grecaptcha"]
|
||||
},
|
||||
"files": [
|
||||
"src/main.browser.ts",
|
||||
|
@@ -4,7 +4,8 @@
|
||||
"outDir": "./out-tsc/app-server",
|
||||
"target": "es2016",
|
||||
"types": [
|
||||
"node"
|
||||
"node",
|
||||
"grecaptcha"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
|
@@ -5,7 +5,8 @@
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
"node",
|
||||
"grecaptcha"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
|
@@ -2247,6 +2247,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7"
|
||||
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":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||
|
Reference in New Issue
Block a user