mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +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(() => {
|
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.
|
||||||
|
@@ -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",
|
||||||
|
@@ -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 }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
|
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
|
* 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[];
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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();
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import { RegisterEmailFormComponent } from './register-email-form.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
RegisterEmailFormComponent,
|
RegisterEmailFormComponent,
|
||||||
|
@@ -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;
|
||||||
@@ -32,7 +34,7 @@ describe('BrowserKlaroService', () => {
|
|||||||
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 }));
|
||||||
|
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
}
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@@ -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
|
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 = [
|
||||||
|
@@ -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",
|
||||||
|
|
||||||
|
@@ -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",
|
||||||
|
@@ -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": [
|
||||||
|
@@ -5,7 +5,8 @@
|
|||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": [
|
||||||
"jasmine",
|
"jasmine",
|
||||||
"node"
|
"node",
|
||||||
|
"grecaptcha"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
@@ -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"
|
||||||
|
Reference in New Issue
Block a user