From 50e849dd44a0c92d857eb1325836cbbb560ddf28 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Fri, 10 Jun 2022 20:48:35 +0530 Subject: [PATCH 01/35] [UXP-10] reCAPTCHA angular component --- package.json | 1 + .../eperson-form/eperson-form.component.ts | 2 +- src/app/core/core.module.ts | 5 ++ .../core/data/eperson-registration.service.ts | 5 +- src/app/core/data/google-recaptcha.service.ts | 15 +++++ src/app/core/shared/registration.model.ts | 12 ++++ .../register-email-form.component.ts | 57 +++++++++++++++---- src/assets/i18n/en.json5 | 2 + src/config/build-config.interface.ts | 1 + src/environments/environment.production.ts | 3 +- src/environments/environment.ts | 3 +- yarn.lock | 13 +++++ 12 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 src/app/core/data/google-recaptcha.service.ts diff --git a/package.json b/package.json index 32832460a2..7c81547574 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "moment": "^2.29.4", "morgan": "^1.10.0", "ng-mocks": "^13.1.1", + "ng-recaptcha": "^9.0.0", "ng2-file-upload": "1.4.0", "ng2-nouislider": "^1.8.3", "ngx-infinite-scroll": "^10.0.1", diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 05fc3189d0..0abcc3d3bc 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -491,7 +491,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ resetPassword() { if (hasValue(this.epersonInitial.email)) { - this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData()) + this.epersonRegistrationService.registerEmail(this.epersonInitial.email, null).pipe(getFirstCompletedRemoteData()) .subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'), diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index b16930e819..1010840d76 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -178,6 +178,8 @@ import { OrcidHistoryDataService } from './orcid/orcid-history-data.service'; import { OrcidQueue } from './orcid/model/orcid-queue.model'; import { OrcidHistory } from './orcid/model/orcid-history.model'; import { OrcidAuthService } from './orcid/orcid-auth.service'; +import { GoogleRecaptchaService } from './data/google-recaptcha.service'; +import { RecaptchaV3Module, RECAPTCHA_V3_SITE_KEY } from 'ng-recaptcha'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -193,6 +195,7 @@ export const restServiceFactory = (mocks: ResponseMapMock, http: HttpClient) => const IMPORTS = [ CommonModule, + RecaptchaV3Module, StoreModule.forFeature('core', coreReducers, storeModuleConfig as StoreConfig), EffectsModule.forFeature(coreEffects) ]; @@ -309,6 +312,8 @@ const PROVIDERS = [ OrcidAuthService, OrcidQueueService, OrcidHistoryDataService, + GoogleRecaptchaService, + { provide: RECAPTCHA_V3_SITE_KEY, useValue: environment.recaptchaSiteKey } ]; /** diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 989a401733..3690b63c33 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -54,9 +54,12 @@ export class EpersonRegistrationService { * Register a new email address * @param email */ - registerEmail(email: string): Observable> { + registerEmail(email: string, captchaToken: string): Observable> { const registration = new Registration(); registration.email = email; + if (captchaToken) { + registration.captchaToken = captchaToken; + } const requestId = this.requestService.generateRequestId(); diff --git a/src/app/core/data/google-recaptcha.service.ts b/src/app/core/data/google-recaptcha.service.ts new file mode 100644 index 0000000000..1865b6ffbb --- /dev/null +++ b/src/app/core/data/google-recaptcha.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { ReCaptchaV3Service } from 'ng-recaptcha'; + +@Injectable() +export class GoogleRecaptchaService { + + constructor( + private recaptchaV3Service: ReCaptchaV3Service + ) {} + + public getRecaptchaToken (action) { + return this.recaptchaV3Service.execute(action); + } + +} diff --git a/src/app/core/shared/registration.model.ts b/src/app/core/shared/registration.model.ts index d679eec0ff..8521bb126e 100644 --- a/src/app/core/shared/registration.model.ts +++ b/src/app/core/shared/registration.model.ts @@ -25,5 +25,17 @@ 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[]; + /** + * The captcha token linked to the registration + */ + captchaToken: string; } diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index d40629f597..4fcc5e6650 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -6,6 +6,12 @@ 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 { GoogleRecaptchaService } from '../core/data/google-recaptcha.service'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; +import { isNotEmpty } from '../shared/empty.util'; +import { map } from 'rxjs/operators'; @Component({ selector: 'ds-register-email-form', @@ -27,12 +33,19 @@ export class RegisterEmailFormComponent implements OnInit { @Input() MESSAGE_PREFIX: string; + /** + * registration verification configuration + */ + registrationVerification = false; + constructor( private epersonRegistrationService: EpersonRegistrationService, private notificationService: NotificationsService, private translateService: TranslateService, private router: Router, - private formBuilder: FormBuilder + private formBuilder: FormBuilder, + private configService: ConfigurationDataService, + private googleRecaptchaService: GoogleRecaptchaService ) { } @@ -45,7 +58,14 @@ export class RegisterEmailFormComponent implements OnInit { ], }) }); - + this.configService.findByPropertyName('registration.verification.enabled').pipe( + getFirstCompletedRemoteData(), + map((res: RemoteData) => { + return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0].toLowerCase() === 'true'; + }) + ).subscribe((res: boolean) => { + this.registrationVerification = res; + }); } /** @@ -53,20 +73,37 @@ export class RegisterEmailFormComponent implements OnInit { */ register() { if (!this.form.invalid) { - this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RemoteData) => { - 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']); + if (this.registrationVerification) { + this.googleRecaptchaService.getRecaptchaToken('register_email').subscribe(res => { + if (isNotEmpty(res)) { + this.registeration(res); } else { 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.recaptcha`, {email: this.email.value})); } - } - ); + }); + } else { + this.registeration(null); + } } } + /** + * Register an email address + */ + registeration(captchaToken) { + this.epersonRegistrationService.registerEmail(this.email.value, captchaToken).subscribe((response: RemoteData) => { + 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})); + } + }); + } + get email() { return this.form.get('email'); } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 57863b3a69..7fd5fe1bcd 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3187,6 +3187,8 @@ "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", + "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items", diff --git a/src/config/build-config.interface.ts b/src/config/build-config.interface.ts index beb8097c9e..1bc4d2a917 100644 --- a/src/config/build-config.interface.ts +++ b/src/config/build-config.interface.ts @@ -3,4 +3,5 @@ import { UniversalConfig } from './universal-config.interface'; export interface BuildConfig extends AppConfig { universal: UniversalConfig; + recaptchaSiteKey: string; } diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index 09b5f19ade..baf7cca29b 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -8,5 +8,6 @@ export const environment: Partial = { preboot: true, async: true, time: false - } + }, + recaptchaSiteKey: '6LfmfEsgAAAAACNqQ0aHqJa0HOHcUsvv2OCiEbV4' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index dc0e808be0..f3017f536a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -13,7 +13,8 @@ export const environment: Partial = { preboot: false, async: true, time: false - } + }, + recaptchaSiteKey: '6LfmfEsgAAAAACNqQ0aHqJa0HOHcUsvv2OCiEbV4' }; /* diff --git a/yarn.lock b/yarn.lock index 1996ace1b1..7780a764b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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.3": + 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" @@ -8858,6 +8863,14 @@ ng-mocks@^13.1.1: resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-13.1.1.tgz#e967dacc420c2ecec71826c5f24e0120186fad0b" integrity sha512-LYi/1ccDwHKLwi4/hvUsmxBDeQ+n8BTdg5f1ujCDCO7OM9OVnqkQcRlBHHK+5iFtEn/aqa2QG3xU/qwIhnaL4w== +ng-recaptcha@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/ng-recaptcha/-/ng-recaptcha-9.0.0.tgz#11b88f820cfca366d363fffd0f451490f2db2b04" + integrity sha512-39YfJh1+p6gvfsGUhC8cmjhFZ1TtQ1OJES5SUgnanPL2aQuwrSX4WyTFh2liFn1dQqtGUVd5O4EhbcexB7AECQ== + dependencies: + "@types/grecaptcha" "^3.0.3" + tslib "^2.2.0" + ng2-file-upload@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.4.0.tgz#8dea28d573234c52af474ad2a4001b335271e5c4" From 2532e370109178557b070aafeaf09bdc958c7b54 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Mon, 13 Jun 2022 18:43:50 +0530 Subject: [PATCH 02/35] [UXP-10] Test cases --- .../eperson-form.component.spec.ts | 2 +- .../data/eperson-registration.service.spec.ts | 2 +- .../register-email-form.component.spec.ts | 51 +++++++++++++++++-- .../register-email-form.component.ts | 10 ++-- src/environments/environment.test.ts | 2 + 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 4957958658..e2bae9a820 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -537,7 +537,7 @@ describe('EPersonFormComponent', () => { }); it('should call epersonRegistrationService.registerEmail', () => { - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail, null); }); }); }); diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index dc13fff3a0..48f5d3cfe5 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -78,7 +78,7 @@ describe('EpersonRegistrationService', () => { describe('registerEmail', () => { it('should send an email registration', () => { - const expected = service.registerEmail('test@mail.org'); + const expected = service.registerEmail('test@mail.org', null); expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); expect(expected).toBeObservable(cold('(a|)', { a: rd })); diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index a415ef4808..040af04f94 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -14,6 +14,8 @@ 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/data/google-recaptcha.service'; describe('RegisterEmailComponent', () => { @@ -24,6 +26,17 @@ describe('RegisterEmailComponent', () => { let epersonRegistrationService: EpersonRegistrationService; let notificationsService; + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: jasmine.createSpy('findByPropertyName') + }); + + const googleRecaptchaService = jasmine.createSpyObj('googleRecaptchaService', { + getRecaptchaToken: jasmine.createSpy('getRecaptchaToken') + }); + + const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['true'] }); + const confResponseDisabled$ = createSuccessfulRemoteDataObject$({ values: ['false'] }); + beforeEach(waitForAsync(() => { router = new RouterStub(); @@ -39,8 +52,10 @@ 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: GoogleRecaptchaService, useValue: googleRecaptchaService}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); @@ -48,6 +63,8 @@ describe('RegisterEmailComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(RegisterEmailFormComponent); comp = fixture.componentInstance; + configurationDataService.findByPropertyName.and.returnValues(confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$); + googleRecaptchaService.getRecaptchaToken.and.returnValue(observableOf('googleRecaptchaToken')); fixture.detectChanges(); }); @@ -71,21 +88,47 @@ describe('RegisterEmailComponent', () => { }); }); describe('register', () => { - it('should send a registration to the service and on success display a message and return to home', () => { + it('should send a registration to the service with google recaptcha and on success display a message and return to home', () => { comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org'); + 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', () => { + it('should send a registration to the service with google recaptcha and on error display a message', () => { (epersonRegistrationService.registerEmail as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 400, 'Bad Request'))); comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken'); + expect(notificationsService.error).toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); + describe('register', () => { + beforeEach(waitForAsync(() => { + configurationDataService.findByPropertyName.and.returnValues(confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$); + comp.ngOnInit(); + fixture.detectChanges(); + })); + + it('should send a registration to the service without google recaptcha and on success display a message and return to home', () => { + comp.form.patchValue({email: 'valid@email.org'}); + + comp.register(); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null); + expect(notificationsService.success).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/home']); + }); + it('should send a registration to the service without google recaptcha and on error display a message', () => { + (epersonRegistrationService.registerEmail as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 400, 'Bad Request'))); + + comp.form.patchValue({email: 'valid@email.org'}); + + comp.register(); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null); expect(notificationsService.error).toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled(); }); diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 4fcc5e6650..b8f8a57353 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -74,9 +74,9 @@ export class RegisterEmailFormComponent implements OnInit { register() { if (!this.form.invalid) { if (this.registrationVerification) { - this.googleRecaptchaService.getRecaptchaToken('register_email').subscribe(res => { - if (isNotEmpty(res)) { - this.registeration(res); + this.googleRecaptchaService.getRecaptchaToken('register_email').subscribe(captcha => { + if (isNotEmpty(captcha)) { + this.registeration(captcha); } else { this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`, {email: this.email.value})); @@ -91,7 +91,7 @@ export class RegisterEmailFormComponent implements OnInit { /** * Register an email address */ - registeration(captchaToken) { + registeration(captchaToken) { this.epersonRegistrationService.registerEmail(this.email.value, captchaToken).subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`), @@ -102,7 +102,7 @@ export class RegisterEmailFormComponent implements OnInit { this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value})); } }); - } + } get email() { return this.form.get('email'); diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 4d466bd37b..d605320af9 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -13,6 +13,8 @@ export const environment: BuildConfig = { time: false }, + recaptchaSiteKey: '6LfmfEsgAAAAACNqQ0aHqJa0HOHcUsvv2OCiEbV4', + // Angular Universal server settings. ui: { ssl: false, From a3eb544422a2a39afd625879de87ce6ee11db252 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Tue, 14 Jun 2022 13:57:49 +0530 Subject: [PATCH 03/35] [UXP-10] Review fixes --- .../eperson-form.component.spec.ts | 2 +- .../eperson-form/eperson-form.component.ts | 2 +- .../data/eperson-registration.service.spec.ts | 2 +- .../core/data/eperson-registration.service.ts | 2 +- .../register-email-form.component.spec.ts | 22 ++++++------- .../register-email-form.component.ts | 33 +++++++++++++------ 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index e2bae9a820..4957958658 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -537,7 +537,7 @@ describe('EPersonFormComponent', () => { }); it('should call epersonRegistrationService.registerEmail', () => { - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail, null); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail); }); }); }); diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 0abcc3d3bc..05fc3189d0 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -491,7 +491,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ resetPassword() { if (hasValue(this.epersonInitial.email)) { - this.epersonRegistrationService.registerEmail(this.epersonInitial.email, null).pipe(getFirstCompletedRemoteData()) + this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData()) .subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'), diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index 48f5d3cfe5..dc13fff3a0 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -78,7 +78,7 @@ describe('EpersonRegistrationService', () => { describe('registerEmail', () => { it('should send an email registration', () => { - const expected = service.registerEmail('test@mail.org', null); + const expected = service.registerEmail('test@mail.org'); expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); expect(expected).toBeObservable(cold('(a|)', { a: rd })); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 3690b63c33..b667d87e04 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -54,7 +54,7 @@ export class EpersonRegistrationService { * Register a new email address * @param email */ - registerEmail(email: string, captchaToken: string): Observable> { + registerEmail(email: string, captchaToken: string = null): Observable> { const registration = new Registration(); registration.email = email; if (captchaToken) { diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index 040af04f94..2d80b6d594 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -63,7 +63,7 @@ describe('RegisterEmailComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(RegisterEmailFormComponent); comp = fixture.componentInstance; - configurationDataService.findByPropertyName.and.returnValues(confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$); + configurationDataService.findByPropertyName.and.returnValues(confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$); googleRecaptchaService.getRecaptchaToken.and.returnValue(observableOf('googleRecaptchaToken')); fixture.detectChanges(); @@ -88,47 +88,47 @@ describe('RegisterEmailComponent', () => { }); }); describe('register', () => { - it('should send a registration to the service with google recaptcha and on success display a message and return to home', () => { + it('should send a registration to the service and on success display a message and return to home', () => { comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org'); expect(notificationsService.success).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith(['/home']); }); - it('should send a registration to the service with google recaptcha and on error display a message', () => { + it('should send a registration to the service and on error display a message', () => { (epersonRegistrationService.registerEmail as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 400, 'Bad Request'))); comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org'); expect(notificationsService.error).toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled(); }); }); - describe('register', () => { + describe('register with google recaptcha', () => { beforeEach(waitForAsync(() => { - configurationDataService.findByPropertyName.and.returnValues(confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$); + configurationDataService.findByPropertyName.and.returnValues(confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$); comp.ngOnInit(); fixture.detectChanges(); })); - it('should send a registration to the service without google recaptcha and on success display a message and return to home', () => { + it('should send a registration to the service and on success display a message and return to home', () => { comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null); + 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 without google recaptcha and on error display a message', () => { + it('should send a registration to the service and on error display a message', () => { (epersonRegistrationService.registerEmail as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 400, 'Bad Request'))); comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken'); expect(notificationsService.error).toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled(); }); diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index b8f8a57353..b124ed2a8c 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -92,16 +92,29 @@ export class RegisterEmailFormComponent implements OnInit { * Register an email address */ registeration(captchaToken) { - this.epersonRegistrationService.registerEmail(this.email.value, captchaToken).subscribe((response: RemoteData) => { - 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 (captchaToken) { + this.epersonRegistrationService.registerEmail(this.email.value, captchaToken).subscribe((response: RemoteData) => { + 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})); + } + }); + } else { + this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RemoteData) => { + 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})); + } + }); + } } get email() { From 0783cd5cb67b214cc2824d1aebe4176eab9bdc4e Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Tue, 14 Jun 2022 14:36:06 +0530 Subject: [PATCH 04/35] [UXP-10] compacting code --- .../register-email-form.component.ts | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index b124ed2a8c..1752865248 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -92,29 +92,20 @@ export class RegisterEmailFormComponent implements OnInit { * Register an email address */ registeration(captchaToken) { + let a = this.epersonRegistrationService.registerEmail(this.email.value); if (captchaToken) { - this.epersonRegistrationService.registerEmail(this.email.value, captchaToken).subscribe((response: RemoteData) => { - 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})); - } - }); - } else { - this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RemoteData) => { - 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})); - } - }); + a = this.epersonRegistrationService.registerEmail(this.email.value, captchaToken); } + a.subscribe((response: RemoteData) => { + 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})); + } + }); } get email() { From 4906516359dadb24b7309021c6bb5a6d64414ddf Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Tue, 14 Jun 2022 20:53:20 +0530 Subject: [PATCH 05/35] [UXP-10] typdocs and tests --- .../data/google-recaptcha.service.spec.ts | 32 +++++++++++++++++++ src/app/core/data/google-recaptcha.service.ts | 7 ++++ 2 files changed, 39 insertions(+) create mode 100644 src/app/core/data/google-recaptcha.service.spec.ts diff --git a/src/app/core/data/google-recaptcha.service.spec.ts b/src/app/core/data/google-recaptcha.service.spec.ts new file mode 100644 index 0000000000..bc13e3e494 --- /dev/null +++ b/src/app/core/data/google-recaptcha.service.spec.ts @@ -0,0 +1,32 @@ +import { GoogleRecaptchaService } from './google-recaptcha.service'; +import { of as observableOf } from 'rxjs'; + +describe('GoogleRecaptchaService', () => { + let service: GoogleRecaptchaService; + + let reCaptchaV3Service; + + function init() { + reCaptchaV3Service = jasmine.createSpyObj('reCaptchaV3Service', { + execute: observableOf('googleRecaptchaToken') + }); + service = new GoogleRecaptchaService(reCaptchaV3Service); + } + + beforeEach(() => { + init(); + }); + + describe('getRecaptchaToken', () => { + let result; + + beforeEach(() => { + result = service.getRecaptchaToken('test'); + }); + + it('should send a Request with action', () => { + expect(reCaptchaV3Service.execute).toHaveBeenCalledWith('test'); + }); + + }); +}); diff --git a/src/app/core/data/google-recaptcha.service.ts b/src/app/core/data/google-recaptcha.service.ts index 1865b6ffbb..edf559d501 100644 --- a/src/app/core/data/google-recaptcha.service.ts +++ b/src/app/core/data/google-recaptcha.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@angular/core'; import { ReCaptchaV3Service } from 'ng-recaptcha'; +/** + * A GoogleRecaptchaService used to send action and get a token from REST + */ @Injectable() export class GoogleRecaptchaService { @@ -8,6 +11,10 @@ export class GoogleRecaptchaService { private recaptchaV3Service: ReCaptchaV3Service ) {} + /** + * 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.recaptchaV3Service.execute(action); } From 095380686597710eb13262a1705842376346db8e Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 16 Jun 2022 18:43:41 +0530 Subject: [PATCH 06/35] script insert and test cases with dynamic site key --- package.json | 2 +- src/app/core/core.module.ts | 5 -- .../data/google-recaptcha.service.spec.ts | 32 --------- src/app/core/data/google-recaptcha.service.ts | 22 ------ .../google-recaptcha.module.ts | 20 ++++++ .../google-recaptcha.service.spec.ts | 47 +++++++++++++ .../google-recaptcha.service.ts | 70 +++++++++++++++++++ .../register-email-form.component.spec.ts | 20 +++--- .../register-email-form.component.ts | 31 ++++---- .../register-email-form.module.ts | 2 + tsconfig.app.json | 2 +- tsconfig.server.json | 3 +- tsconfig.spec.json | 3 +- yarn.lock | 10 +-- 14 files changed, 172 insertions(+), 97 deletions(-) delete mode 100644 src/app/core/data/google-recaptcha.service.spec.ts delete mode 100644 src/app/core/data/google-recaptcha.service.ts create mode 100644 src/app/core/google-recaptcha/google-recaptcha.module.ts create mode 100644 src/app/core/google-recaptcha/google-recaptcha.service.spec.ts create mode 100644 src/app/core/google-recaptcha/google-recaptcha.service.ts diff --git a/package.json b/package.json index 7c81547574..31cc8d7b23 100644 --- a/package.json +++ b/package.json @@ -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", @@ -110,7 +111,6 @@ "moment": "^2.29.4", "morgan": "^1.10.0", "ng-mocks": "^13.1.1", - "ng-recaptcha": "^9.0.0", "ng2-file-upload": "1.4.0", "ng2-nouislider": "^1.8.3", "ngx-infinite-scroll": "^10.0.1", diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 1010840d76..b16930e819 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -178,8 +178,6 @@ import { OrcidHistoryDataService } from './orcid/orcid-history-data.service'; import { OrcidQueue } from './orcid/model/orcid-queue.model'; import { OrcidHistory } from './orcid/model/orcid-history.model'; import { OrcidAuthService } from './orcid/orcid-auth.service'; -import { GoogleRecaptchaService } from './data/google-recaptcha.service'; -import { RecaptchaV3Module, RECAPTCHA_V3_SITE_KEY } from 'ng-recaptcha'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -195,7 +193,6 @@ export const restServiceFactory = (mocks: ResponseMapMock, http: HttpClient) => const IMPORTS = [ CommonModule, - RecaptchaV3Module, StoreModule.forFeature('core', coreReducers, storeModuleConfig as StoreConfig), EffectsModule.forFeature(coreEffects) ]; @@ -312,8 +309,6 @@ const PROVIDERS = [ OrcidAuthService, OrcidQueueService, OrcidHistoryDataService, - GoogleRecaptchaService, - { provide: RECAPTCHA_V3_SITE_KEY, useValue: environment.recaptchaSiteKey } ]; /** diff --git a/src/app/core/data/google-recaptcha.service.spec.ts b/src/app/core/data/google-recaptcha.service.spec.ts deleted file mode 100644 index bc13e3e494..0000000000 --- a/src/app/core/data/google-recaptcha.service.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GoogleRecaptchaService } from './google-recaptcha.service'; -import { of as observableOf } from 'rxjs'; - -describe('GoogleRecaptchaService', () => { - let service: GoogleRecaptchaService; - - let reCaptchaV3Service; - - function init() { - reCaptchaV3Service = jasmine.createSpyObj('reCaptchaV3Service', { - execute: observableOf('googleRecaptchaToken') - }); - service = new GoogleRecaptchaService(reCaptchaV3Service); - } - - beforeEach(() => { - init(); - }); - - describe('getRecaptchaToken', () => { - let result; - - beforeEach(() => { - result = service.getRecaptchaToken('test'); - }); - - it('should send a Request with action', () => { - expect(reCaptchaV3Service.execute).toHaveBeenCalledWith('test'); - }); - - }); -}); diff --git a/src/app/core/data/google-recaptcha.service.ts b/src/app/core/data/google-recaptcha.service.ts deleted file mode 100644 index edf559d501..0000000000 --- a/src/app/core/data/google-recaptcha.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ReCaptchaV3Service } from 'ng-recaptcha'; - -/** - * A GoogleRecaptchaService used to send action and get a token from REST - */ -@Injectable() -export class GoogleRecaptchaService { - - constructor( - private recaptchaV3Service: ReCaptchaV3Service - ) {} - - /** - * 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.recaptchaV3Service.execute(action); - } - -} diff --git a/src/app/core/google-recaptcha/google-recaptcha.module.ts b/src/app/core/google-recaptcha/google-recaptcha.module.ts new file mode 100644 index 0000000000..e2acba0e93 --- /dev/null +++ b/src/app/core/google-recaptcha/google-recaptcha.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../../shared/shared.module'; + +import { GoogleRecaptchaService } from './google-recaptcha.service'; + +const PROVIDERS = [ + GoogleRecaptchaService +]; + +@NgModule({ + imports: [ SharedModule ], + providers: [ + ...PROVIDERS + ] +}) + +/** + * This module handles google recaptcha functionalities + */ +export class GoogleRecaptchaModule {} diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts b/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts new file mode 100644 index 0000000000..3698306763 --- /dev/null +++ b/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts @@ -0,0 +1,47 @@ +import { GoogleRecaptchaService } from './google-recaptcha.service'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; + +describe('GoogleRecaptchaService', () => { + let service: GoogleRecaptchaService; + + let rendererFactory2; + let configurationDataService; + let spy: jasmine.Spy; + let scriptElementMock: any; + const innerHTMLTestValue = 'mock-script-inner-html'; + const document = { documentElement: { lang: 'en' } } as Document; + scriptElementMock = { + set innerHTML(newVal) { /* noop */ }, + get innerHTML() { return innerHTMLTestValue; } + }; + + function init() { + rendererFactory2 = jasmine.createSpyObj('rendererFactory2', { + createRenderer: observableOf('googleRecaptchaToken'), + createElement: scriptElementMock + }); + configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] }) + }); + service = new GoogleRecaptchaService(document, 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'); + }); + + }); +}); diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts new file mode 100644 index 0000000000..168e385597 --- /dev/null +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -0,0 +1,70 @@ +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'; + +/** + * A GoogleRecaptchaService used to send action and get a token from REST + */ +@Injectable() +export class GoogleRecaptchaService { + + private renderer: Renderer2; + captchaSiteKey: string; + + constructor( + @Inject(DOCUMENT) private _document: Document, + rendererFactory: RendererFactory2, + private configService: ConfigurationDataService, + ) { + this.renderer = rendererFactory.createRenderer(null, null); + this.configService.findByPropertyName('google.recaptcha.key').pipe( + getFirstCompletedRemoteData(), + ).subscribe((res: RemoteData) => { + if (res.hasSucceeded && isNotEmpty(res?.payload?.values[0])) { + this.captchaSiteKey = res?.payload?.values[0]; + this.loadScript(this.buildCaptchaUrl(res?.payload?.values[0])); + } + }); + } + + /** + * Returns an observable of string + * @param action action is the process type in which used to protect multiple spam REST calls + */ + public async getRecaptchaToken (action) { + return await grecaptcha.execute(this.captchaSiteKey, {action: action}); + } + + /** + * 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) { + return `https://www.google.com/recaptcha/api.js?render=${key}`; + } + + /** + * 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); + }); + } + +} diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index 2d80b6d594..330933426a 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -1,4 +1,4 @@ -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 { RestResponse } from '../core/cache/response.models'; import { CommonModule } from '@angular/common'; @@ -15,7 +15,7 @@ import { NotificationsServiceStub } from '../shared/testing/notifications-servic 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/data/google-recaptcha.service'; +import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; describe('RegisterEmailComponent', () => { @@ -31,7 +31,7 @@ describe('RegisterEmailComponent', () => { }); const googleRecaptchaService = jasmine.createSpyObj('googleRecaptchaService', { - getRecaptchaToken: jasmine.createSpy('getRecaptchaToken') + getRecaptchaToken: Promise.resolve('googleRecaptchaToken') }); const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['true'] }); @@ -64,7 +64,6 @@ describe('RegisterEmailComponent', () => { fixture = TestBed.createComponent(RegisterEmailFormComponent); comp = fixture.componentInstance; configurationDataService.findByPropertyName.and.returnValues(confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$); - googleRecaptchaService.getRecaptchaToken.and.returnValue(observableOf('googleRecaptchaToken')); fixture.detectChanges(); }); @@ -108,29 +107,30 @@ describe('RegisterEmailComponent', () => { }); }); describe('register with google recaptcha', () => { - beforeEach(waitForAsync(() => { + beforeEach(fakeAsync(() => { configurationDataService.findByPropertyName.and.returnValues(confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$); comp.ngOnInit(); fixture.detectChanges(); })); - it('should send a registration to the service and on success display a message and return to home', () => { + 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', () => { + })); + 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(); - }); + })); }); }); diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 1752865248..c53fbd993d 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -6,12 +6,12 @@ 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 { GoogleRecaptchaService } from '../core/data/google-recaptcha.service'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { isNotEmpty } from '../shared/empty.util'; import { map } from 'rxjs/operators'; +import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; @Component({ selector: 'ds-register-email-form', @@ -71,19 +71,18 @@ export class RegisterEmailFormComponent implements OnInit { /** * Register an email address */ - register() { + async register() { if (!this.form.invalid) { if (this.registrationVerification) { - this.googleRecaptchaService.getRecaptchaToken('register_email').subscribe(captcha => { - if (isNotEmpty(captcha)) { - this.registeration(captcha); - } else { - this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), - this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`, {email: this.email.value})); - } - }); + const token = await this.googleRecaptchaService.getRecaptchaToken('register_email'); + if (isNotEmpty(token)) { + this.registeration(token); + } else { + this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), + this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`, {email: this.email.value})); + } } else { - this.registeration(null); + this.registeration(); } } } @@ -91,12 +90,14 @@ export class RegisterEmailFormComponent implements OnInit { /** * Register an email address */ - registeration(captchaToken) { - let a = this.epersonRegistrationService.registerEmail(this.email.value); + registeration(captchaToken = null) { + let registerEmail$; if (captchaToken) { - a = this.epersonRegistrationService.registerEmail(this.email.value, captchaToken); + registerEmail$ = this.epersonRegistrationService.registerEmail(this.email.value, captchaToken); + } else { + registerEmail$ = this.epersonRegistrationService.registerEmail(this.email.value); } - a.subscribe((response: RemoteData) => { + registerEmail$.subscribe((response: RemoteData) => { 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})); diff --git a/src/app/register-email-form/register-email-form.module.ts b/src/app/register-email-form/register-email-form.module.ts index f19e869beb..18c2830ff4 100644 --- a/src/app/register-email-form/register-email-form.module.ts +++ b/src/app/register-email-form/register-email-form.module.ts @@ -2,11 +2,13 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; import { RegisterEmailFormComponent } from './register-email-form.component'; +import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module'; @NgModule({ imports: [ CommonModule, SharedModule, + GoogleRecaptchaModule ], declarations: [ RegisterEmailFormComponent, diff --git a/tsconfig.app.json b/tsconfig.app.json index f67e8b25d8..c89834db4a 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": ["grecaptcha"] }, "files": [ "src/main.browser.ts", diff --git a/tsconfig.server.json b/tsconfig.server.json index 76be339fca..d2fb2c9d40 100644 --- a/tsconfig.server.json +++ b/tsconfig.server.json @@ -4,7 +4,8 @@ "outDir": "./out-tsc/app-server", "target": "es2016", "types": [ - "node" + "node", + "grecaptcha" ] }, "files": [ diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 05d55a31f8..0d92677545 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -5,7 +5,8 @@ "outDir": "./out-tsc/spec", "types": [ "jasmine", - "node" + "node", + "grecaptcha" ] }, "files": [ diff --git a/yarn.lock b/yarn.lock index 7780a764b6..1c262f83d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2247,7 +2247,7 @@ resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7" integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ== -"@types/grecaptcha@^3.0.3": +"@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== @@ -8863,14 +8863,6 @@ ng-mocks@^13.1.1: resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-13.1.1.tgz#e967dacc420c2ecec71826c5f24e0120186fad0b" integrity sha512-LYi/1ccDwHKLwi4/hvUsmxBDeQ+n8BTdg5f1ujCDCO7OM9OVnqkQcRlBHHK+5iFtEn/aqa2QG3xU/qwIhnaL4w== -ng-recaptcha@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/ng-recaptcha/-/ng-recaptcha-9.0.0.tgz#11b88f820cfca366d363fffd0f451490f2db2b04" - integrity sha512-39YfJh1+p6gvfsGUhC8cmjhFZ1TtQ1OJES5SUgnanPL2aQuwrSX4WyTFh2liFn1dQqtGUVd5O4EhbcexB7AECQ== - dependencies: - "@types/grecaptcha" "^3.0.3" - tslib "^2.2.0" - ng2-file-upload@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.4.0.tgz#8dea28d573234c52af474ad2a4001b335271e5c4" From 2ef701a231f37edad67c696a39b725f87c63b231 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 16 Jun 2022 18:46:41 +0530 Subject: [PATCH 07/35] remove site key from environment --- src/config/build-config.interface.ts | 1 - src/environments/environment.production.ts | 3 +-- src/environments/environment.test.ts | 2 -- src/environments/environment.ts | 3 +-- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/config/build-config.interface.ts b/src/config/build-config.interface.ts index 1bc4d2a917..beb8097c9e 100644 --- a/src/config/build-config.interface.ts +++ b/src/config/build-config.interface.ts @@ -3,5 +3,4 @@ import { UniversalConfig } from './universal-config.interface'; export interface BuildConfig extends AppConfig { universal: UniversalConfig; - recaptchaSiteKey: string; } diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index baf7cca29b..09b5f19ade 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -8,6 +8,5 @@ export const environment: Partial = { preboot: true, async: true, time: false - }, - recaptchaSiteKey: '6LfmfEsgAAAAACNqQ0aHqJa0HOHcUsvv2OCiEbV4' + } }; diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index d605320af9..4d466bd37b 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -13,8 +13,6 @@ export const environment: BuildConfig = { time: false }, - recaptchaSiteKey: '6LfmfEsgAAAAACNqQ0aHqJa0HOHcUsvv2OCiEbV4', - // Angular Universal server settings. ui: { ssl: false, diff --git a/src/environments/environment.ts b/src/environments/environment.ts index f3017f536a..dc0e808be0 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -13,8 +13,7 @@ export const environment: Partial = { preboot: false, async: true, time: false - }, - recaptchaSiteKey: '6LfmfEsgAAAAACNqQ0aHqJa0HOHcUsvv2OCiEbV4' + } }; /* From fcad492a25a5f31492a3d36e54d7da180deae95a Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 7 Jul 2022 14:38:10 +0530 Subject: [PATCH 08/35] [UXP-10] token passing in header --- .../data/eperson-registration.service.spec.ts | 19 ++++++++++++- .../core/data/eperson-registration.service.ts | 14 +++++++--- .../google-recaptcha.service.ts | 28 ++++++++++++++----- src/app/core/shared/registration.model.ts | 5 ---- .../register-email-form.component.ts | 8 +++--- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index dc13fff3a0..c7785302ef 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -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 })); }); }); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index b667d87e04..6f64879e0b 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -12,6 +12,8 @@ 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( { @@ -57,18 +59,22 @@ export class EpersonRegistrationService { registerEmail(email: string, captchaToken: string = null): Observable> { const registration = new Registration(); registration.email = email; - if (captchaToken) { - registration.captchaToken = captchaToken; - } const requestId = this.requestService.generateRequestId(); 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(); diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts index 168e385597..46611e5ebb 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -5,6 +5,8 @@ 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 } from 'rxjs/operators'; +import { combineLatest } from 'rxjs'; /** * A GoogleRecaptchaService used to send action and get a token from REST @@ -13,6 +15,9 @@ import { RemoteData } from '../data/remote-data'; export class GoogleRecaptchaService { private renderer: Renderer2; + /** + * A Google Recaptcha site key + */ captchaSiteKey: string; constructor( @@ -21,12 +26,21 @@ export class GoogleRecaptchaService { private configService: ConfigurationDataService, ) { this.renderer = rendererFactory.createRenderer(null, null); - this.configService.findByPropertyName('google.recaptcha.key').pipe( + const registrationVerification$ = this.configService.findByPropertyName('registration.verification.enabled').pipe( getFirstCompletedRemoteData(), - ).subscribe((res: RemoteData) => { - if (res.hasSucceeded && isNotEmpty(res?.payload?.values[0])) { - this.captchaSiteKey = res?.payload?.values[0]; - this.loadScript(this.buildCaptchaUrl(res?.payload?.values[0])); + map((res: RemoteData) => { + return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0].toLowerCase() === 'true'; + }) + ); + const recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe( + getFirstCompletedRemoteData(), + ); + combineLatest(registrationVerification$, recaptchaKey$).subscribe(([registrationVerification, recaptchaKey]) => { + if (registrationVerification) { + if (recaptchaKey.hasSucceeded && isNotEmpty(recaptchaKey?.payload?.values[0])) { + this.captchaSiteKey = recaptchaKey?.payload?.values[0]; + this.loadScript(this.buildCaptchaUrl(recaptchaKey?.payload?.values[0])); + } } }); } @@ -45,7 +59,7 @@ export class GoogleRecaptchaService { * @param key contains a secret key of a google captchas * @returns string which has google captcha url with google captchas key */ - buildCaptchaUrl(key: string) { + buildCaptchaUrl(key: string) { return `https://www.google.com/recaptcha/api.js?render=${key}`; } @@ -55,7 +69,7 @@ export class GoogleRecaptchaService { * @param url contains a script url which will be loaded into page * @returns A promise */ - private loadScript(url) { + private loadScript(url) { return new Promise((resolve, reject) => { const script = this.renderer.createElement('script'); script.type = 'text/javascript'; diff --git a/src/app/core/shared/registration.model.ts b/src/app/core/shared/registration.model.ts index 8521bb126e..bc4488964f 100644 --- a/src/app/core/shared/registration.model.ts +++ b/src/app/core/shared/registration.model.ts @@ -33,9 +33,4 @@ export class Registration implements UnCacheableObject { * The token linked to the registration */ groups: string[]; - - /** - * The captcha token linked to the registration - */ - captchaToken: string; } diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index c53fbd993d..04081dc7ff 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -76,21 +76,21 @@ export class RegisterEmailFormComponent implements OnInit { if (this.registrationVerification) { const token = await this.googleRecaptchaService.getRecaptchaToken('register_email'); if (isNotEmpty(token)) { - this.registeration(token); + this.registration(token); } else { this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`, {email: this.email.value})); } } else { - this.registeration(); + this.registration(); } } } /** - * Register an email address + * Registration of an email address */ - registeration(captchaToken = null) { + registration(captchaToken = null) { let registerEmail$; if (captchaToken) { registerEmail$ = this.epersonRegistrationService.registerEmail(this.email.value, captchaToken); From e295dccc8a884083030d34d613c832da40598595 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Tue, 16 Aug 2022 20:38:48 +0530 Subject: [PATCH 09/35] [UXP-10] dynamic recaptcha versions and modes --- src/app/app.component.ts | 5 +- .../google-recaptcha.module.ts | 16 +++-- .../google-recaptcha.service.ts | 70 +++++++++++++++++-- .../register-email-form.component.html | 6 +- .../register-email-form.component.spec.ts | 12 +++- .../register-email-form.component.ts | 44 ++++++++++-- .../register-email-form.module.ts | 4 +- src/app/shared/cookies/klaro-configuration.ts | 10 +++ .../google-recaptcha.component.html | 4 ++ .../google-recaptcha.component.scss | 0 .../google-recaptcha.component.spec.ts | 50 +++++++++++++ .../google-recaptcha.component.ts | 45 ++++++++++++ src/app/shared/shared.module.ts | 4 +- src/assets/i18n/en.json5 | 17 +++++ 14 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 src/app/shared/google-recaptcha/google-recaptcha.component.html create mode 100644 src/app/shared/google-recaptcha/google-recaptcha.component.scss create mode 100644 src/app/shared/google-recaptcha/google-recaptcha.component.spec.ts create mode 100644 src/app/shared/google-recaptcha/google-recaptcha.component.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ee8c4d685f..68c1bce6f3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -48,8 +48,8 @@ import { BASE_THEME_NAME } from './shared/theme-support/theme.constants'; import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; import { getDefaultThemeConfig } from '../config/config.util'; -import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; -import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface'; +import { AppConfig, APP_CONFIG } from '../config/app-config.interface'; +import { GoogleRecaptchaService } from './core/google-recaptcha/google-recaptcha.service'; @Component({ selector: 'ds-app', @@ -110,6 +110,7 @@ export class AppComponent implements OnInit, AfterViewInit { private modalConfig: NgbModalConfig, @Optional() private cookiesService: KlaroService, @Optional() private googleAnalyticsService: GoogleAnalyticsService, + @Optional() private googleRecaptchaService: GoogleRecaptchaService, ) { if (!isEqual(environment, this.appConfig)) { diff --git a/src/app/core/google-recaptcha/google-recaptcha.module.ts b/src/app/core/google-recaptcha/google-recaptcha.module.ts index e2acba0e93..8af9adb641 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.module.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.module.ts @@ -1,17 +1,23 @@ +import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { GoogleRecaptchaComponent } from '../../shared/google-recaptcha/google-recaptcha.component'; import { SharedModule } from '../../shared/shared.module'; import { GoogleRecaptchaService } from './google-recaptcha.service'; const PROVIDERS = [ - GoogleRecaptchaService + GoogleRecaptchaService +]; + +const COMPONENTS = [ + GoogleRecaptchaComponent ]; @NgModule({ - imports: [ SharedModule ], - providers: [ - ...PROVIDERS - ] + imports: [ CommonModule ], + providers: [...PROVIDERS], + declarations: [...COMPONENTS], + exports: [...COMPONENTS] }) /** diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts index 46611e5ebb..99311247f6 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -5,8 +5,10 @@ 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 } from 'rxjs/operators'; -import { combineLatest } from 'rxjs'; +import { map, take } from 'rxjs/operators'; +import { combineLatest, Observable, of } from 'rxjs'; + +export const CAPTCHA_COOKIE = '_GRECAPTCHA'; /** * A GoogleRecaptchaService used to send action and get a token from REST @@ -18,7 +20,22 @@ export class GoogleRecaptchaService { /** * A Google Recaptcha site key */ - captchaSiteKey: string; + captchaSiteKeyStr: string; + + /** + * A Google Recaptcha site key + */ + captchaSiteKey$: Observable; + + /** + * A Google Recaptcha mode + */ + captchaMode$: Observable = of('invisible'); + + /** + * A Google Recaptcha version + */ + captchaVersion$: Observable; constructor( @Inject(DOCUMENT) private _document: Document, @@ -27,19 +44,43 @@ export class GoogleRecaptchaService { ) { this.renderer = rendererFactory.createRenderer(null, null); const registrationVerification$ = this.configService.findByPropertyName('registration.verification.enabled').pipe( + take(1), getFirstCompletedRemoteData(), map((res: RemoteData) => { return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0].toLowerCase() === 'true'; }) ); const recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe( + take(1), getFirstCompletedRemoteData(), ); - combineLatest(registrationVerification$, recaptchaKey$).subscribe(([registrationVerification, recaptchaKey]) => { + const recaptchaVersion$ = this.configService.findByPropertyName('google.recaptcha.version').pipe( + take(1), + getFirstCompletedRemoteData(), + ); + const recaptchaMode$ = this.configService.findByPropertyName('google.recaptcha.mode').pipe( + take(1), + getFirstCompletedRemoteData(), + ); + combineLatest(registrationVerification$, recaptchaVersion$, recaptchaMode$, recaptchaKey$).subscribe(([registrationVerification, recaptchaVersion, recaptchaMode, recaptchaKey]) => { if (registrationVerification) { if (recaptchaKey.hasSucceeded && isNotEmpty(recaptchaKey?.payload?.values[0])) { - this.captchaSiteKey = recaptchaKey?.payload?.values[0]; - this.loadScript(this.buildCaptchaUrl(recaptchaKey?.payload?.values[0])); + this.captchaSiteKeyStr = recaptchaKey?.payload?.values[0]; + this.captchaSiteKey$ = of(recaptchaKey?.payload?.values[0]); + } + + if (recaptchaVersion.hasSucceeded && isNotEmpty(recaptchaVersion?.payload?.values[0]) && recaptchaVersion?.payload?.values[0] === 'v3') { + this.captchaVersion$ = of('v3'); + if (recaptchaKey.hasSucceeded && isNotEmpty(recaptchaKey?.payload?.values[0])) { + this.loadScript(this.buildCaptchaUrl(recaptchaKey?.payload?.values[0])); + } + } else { + this.captchaVersion$ = of('v2'); + const captchaUrl = 'https://www.google.com/recaptcha/api.js'; + if (recaptchaMode.hasSucceeded && isNotEmpty(recaptchaMode?.payload?.values[0])) { + this.captchaMode$ = of(recaptchaMode?.payload?.values[0]); + this.loadScript(captchaUrl); + } } } }); @@ -50,7 +91,22 @@ export class GoogleRecaptchaService { * @param action action is the process type in which used to protect multiple spam REST calls */ public async getRecaptchaToken (action) { - return await grecaptcha.execute(this.captchaSiteKey, {action: action}); + return await grecaptcha.execute(this.captchaSiteKeyStr, {action: action}); + } + + /** + * Returns an observable of string + */ + public async executeRecaptcha () { + return await grecaptcha.execute(); + } + + /** + * Returns an observable of string + * @param action action is the process type in which used to protect multiple spam REST calls + */ + public async getRecaptchaTokenResponse () { + return await grecaptcha.getResponse(); } /** diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index e47eedb6ae..5e87d2bd42 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -24,6 +24,9 @@
{{MESSAGE_PREFIX + '.email.hint' |translate}}
+
+ +
@@ -32,5 +35,6 @@ + (click)="(captchaVersion === 'v2' && captchaMode === 'invisible') ? executeRecaptcha() : register()"> + {{MESSAGE_PREFIX + '.submit'| translate}} diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index 330933426a..55004c044b 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -1,5 +1,5 @@ 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 { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; @@ -31,9 +31,13 @@ describe('RegisterEmailComponent', () => { }); const googleRecaptchaService = jasmine.createSpyObj('googleRecaptchaService', { - getRecaptchaToken: Promise.resolve('googleRecaptchaToken') + getRecaptchaToken: Promise.resolve('googleRecaptchaToken'), + executeRecaptcha: Promise.resolve('googleRecaptchaToken'), + getRecaptchaTokenResponse: Promise.resolve('googleRecaptchaToken') }); + const captchaVersion$ = of('v3'); + const captchaMode$ = of('invisible'); const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['true'] }); const confResponseDisabled$ = createSuccessfulRemoteDataObject$({ values: ['false'] }); @@ -63,6 +67,8 @@ 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(); @@ -109,6 +115,8 @@ describe('RegisterEmailComponent', () => { 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(); })); diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 04081dc7ff..79303c8a04 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -7,11 +7,12 @@ 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 { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { isNotEmpty } from '../shared/empty.util'; import { map } from 'rxjs/operators'; import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; +import { Observable } from 'rxjs'; @Component({ selector: 'ds-register-email-form', @@ -38,6 +39,18 @@ export class RegisterEmailFormComponent implements OnInit { */ registrationVerification = false; + /** + * captcha version + */ + captchaVersion = 'v2'; + + /** + * captcha mode + */ + captchaMode = 'checkbox'; + + recaptchaKey$: Observable; + constructor( private epersonRegistrationService: EpersonRegistrationService, private notificationService: NotificationsService, @@ -45,7 +58,7 @@ export class RegisterEmailFormComponent implements OnInit { private router: Router, private formBuilder: FormBuilder, private configService: ConfigurationDataService, - private googleRecaptchaService: GoogleRecaptchaService + public googleRecaptchaService: GoogleRecaptchaService ) { } @@ -58,6 +71,15 @@ export class RegisterEmailFormComponent implements OnInit { ], }) }); + this.recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe( + getFirstSucceededRemoteDataPayload(), + ); + this.googleRecaptchaService.captchaVersion$.subscribe(res => { + this.captchaVersion = res; + }); + this.googleRecaptchaService.captchaMode$.subscribe(res => { + this.captchaMode = res; + }); this.configService.findByPropertyName('registration.verification.enabled').pipe( getFirstCompletedRemoteData(), map((res: RemoteData) => { @@ -68,13 +90,27 @@ export class RegisterEmailFormComponent implements OnInit { }); } + /** + * execute the captcha function for v2 invisible + */ + async executeRecaptcha() { + await this.googleRecaptchaService.executeRecaptcha(); + } + /** * Register an email address */ - async register() { + async register(tokenV2 = null) { if (!this.form.invalid) { if (this.registrationVerification) { - const token = await this.googleRecaptchaService.getRecaptchaToken('register_email'); + let token; + if (this.captchaVersion === 'v3') { + token = await this.googleRecaptchaService.getRecaptchaToken('register_email'); + } else if (this.captchaMode === 'checkbox') { + token = await this.googleRecaptchaService.getRecaptchaTokenResponse(); + } else { + token = tokenV2; + } if (isNotEmpty(token)) { this.registration(token); } else { diff --git a/src/app/register-email-form/register-email-form.module.ts b/src/app/register-email-form/register-email-form.module.ts index 18c2830ff4..a765759413 100644 --- a/src/app/register-email-form/register-email-form.module.ts +++ b/src/app/register-email-form/register-email-form.module.ts @@ -2,13 +2,11 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; import { RegisterEmailFormComponent } from './register-email-form.component'; -import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module'; @NgModule({ imports: [ CommonModule, - SharedModule, - GoogleRecaptchaModule + SharedModule ], declarations: [ RegisterEmailFormComponent, diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index 659583ad87..1f780198f4 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -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 } from 'src/app/core/google-recaptcha/google-recaptcha.service'; /** * Cookie for has_agreed_end_user @@ -155,5 +156,14 @@ export const klaroConfiguration: any = { */ onlyOnce: true, }, + { + name: 'google-recaptcha', + purposes: ['registration-password-recovery'], + required: true, + cookies: [ + CAPTCHA_COOKIE + ], + onlyOnce: true, + } ], }; diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.html b/src/app/shared/google-recaptcha/google-recaptcha.component.html new file mode 100644 index 0000000000..50bc51c808 --- /dev/null +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.html @@ -0,0 +1,4 @@ +
\ No newline at end of file diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.scss b/src/app/shared/google-recaptcha/google-recaptcha.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.spec.ts b/src/app/shared/google-recaptcha/google-recaptcha.component.spec.ts new file mode 100644 index 0000000000..67f66c9757 --- /dev/null +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.spec.ts @@ -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; + + + 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(); + }); +}); diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.ts b/src/app/shared/google-recaptcha/google-recaptcha.component.ts new file mode 100644 index 0000000000..f9c9b599f6 --- /dev/null +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.ts @@ -0,0 +1,45 @@ +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'; + +@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 = new EventEmitter(); + + recaptchaKey$: Observable; + + 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(), + ); + if (this.captchaMode === 'invisible') { + this._window.nativeWindow.executeRecaptcha = this.execute; + } + } + + execute = (event) => { + this.executeRecaptcha.emit(event); + }; + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index f40ddd5b90..9ca2f22fa4 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -293,6 +293,7 @@ import { BrowserOnlyPipe } from './utils/browser-only.pipe'; import { ThemedLoadingComponent } from './loading/themed-loading.component'; import { PersonPageClaimButtonComponent } from './dso-page/person-page-claim-button/person-page-claim-button.component'; import { SearchExportCsvComponent } from './search/search-export-csv/search-export-csv.component'; +import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module'; const MODULES = [ CommonModule, @@ -313,7 +314,8 @@ const MODULES = [ NouisliderModule, MomentModule, DragDropModule, - CdkTreeModule + CdkTreeModule, + GoogleRecaptchaModule ]; const ROOT_MODULES = [ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 7fd5fe1bcd..7aa884866f 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1262,10 +1262,27 @@ + "cookies.consent.app.title.google-recaptcha": "Google reCaptcha", + + "cookies.consent.app.description.google-recaptcha": "Allows us to track registration and password recovery data", + + + "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", + + "cris-layout.toggle.open": "Open section", + + "cris-layout.toggle.close": "Close section", + + "cris-layout.toggle.aria.open": "Expand {{sectionHeader}} section", + + "cris-layout.toggle.aria.close": "Collapse {{sectionHeader}} section", "curation-task.task.checklinks.label": "Check Links in Metadata", From bcc747dc3eecd16ed93a9b6b0831715dfa57f1ee Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Mon, 22 Aug 2022 15:03:35 +0530 Subject: [PATCH 10/35] [UXP-10] klaro cookies permission and test cases --- .../google-recaptcha.service.spec.ts | 12 +++++++- .../google-recaptcha.service.ts | 27 +++++++++++++++--- .../register-email-form.component.html | 14 +++++++--- .../register-email-form.component.ts | 28 +++++++------------ src/app/shared/cookies/klaro-configuration.ts | 9 ++++-- src/assets/i18n/en.json5 | 2 +- 6 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts b/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts index 3698306763..545e3b9873 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts @@ -1,6 +1,7 @@ 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; @@ -9,6 +10,8 @@ describe('GoogleRecaptchaService', () => { 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 = { @@ -17,6 +20,7 @@ describe('GoogleRecaptchaService', () => { }; function init() { + window = new NativeWindowRef(); rendererFactory2 = jasmine.createSpyObj('rendererFactory2', { createRenderer: observableOf('googleRecaptchaToken'), createElement: scriptElementMock @@ -24,7 +28,13 @@ describe('GoogleRecaptchaService', () => { configurationDataService = jasmine.createSpyObj('configurationDataService', { findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] }) }); - service = new GoogleRecaptchaService(document, rendererFactory2, configurationDataService); + 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(() => { diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts index 99311247f6..4f602ef575 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -7,8 +7,11 @@ import { ConfigurationDataService } from '../data/configuration-data.service'; import { RemoteData } from '../data/remote-data'; import { map, take } from 'rxjs/operators'; import { 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 @@ -35,13 +38,18 @@ export class GoogleRecaptchaService { /** * A Google Recaptcha version */ - captchaVersion$: Observable; + captchaVersion$: Observable = of(''); 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), @@ -50,6 +58,14 @@ export class GoogleRecaptchaService { return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0].toLowerCase() === 'true'; }) ); + registrationVerification$.subscribe(registrationVerification => { + if (registrationVerification) { + this.loadRecaptchaProperties(); + } + }); + } + + loadRecaptchaProperties() { const recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe( take(1), getFirstCompletedRemoteData(), @@ -62,13 +78,12 @@ export class GoogleRecaptchaService { take(1), getFirstCompletedRemoteData(), ); - combineLatest(registrationVerification$, recaptchaVersion$, recaptchaMode$, recaptchaKey$).subscribe(([registrationVerification, recaptchaVersion, recaptchaMode, recaptchaKey]) => { - if (registrationVerification) { + combineLatest(recaptchaVersion$, recaptchaMode$, recaptchaKey$).subscribe(([recaptchaVersion, recaptchaMode, recaptchaKey]) => { + if (this.cookieService.get('klaro-anonymous') && this.cookieService.get('klaro-anonymous')[CAPTCHA_NAME]) { if (recaptchaKey.hasSucceeded && isNotEmpty(recaptchaKey?.payload?.values[0])) { this.captchaSiteKeyStr = recaptchaKey?.payload?.values[0]; this.captchaSiteKey$ = of(recaptchaKey?.payload?.values[0]); } - if (recaptchaVersion.hasSucceeded && isNotEmpty(recaptchaVersion?.payload?.values[0]) && recaptchaVersion?.payload?.values[0] === 'v3') { this.captchaVersion$ = of('v3'); if (recaptchaKey.hasSucceeded && isNotEmpty(recaptchaKey?.payload?.values[0])) { @@ -137,4 +152,8 @@ export class GoogleRecaptchaService { }); } + refreshCaptchaScript = () => { + this.loadRecaptchaProperties(); + }; + } diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index 5e87d2bd42..b10833c097 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -24,8 +24,8 @@
{{MESSAGE_PREFIX + '.email.hint' |translate}}
-
- +
+
@@ -33,8 +33,14 @@ - + diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 79303c8a04..0079ea2da8 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -39,16 +39,6 @@ export class RegisterEmailFormComponent implements OnInit { */ registrationVerification = false; - /** - * captcha version - */ - captchaVersion = 'v2'; - - /** - * captcha mode - */ - captchaMode = 'checkbox'; - recaptchaKey$: Observable; constructor( @@ -74,12 +64,6 @@ export class RegisterEmailFormComponent implements OnInit { this.recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe( getFirstSucceededRemoteDataPayload(), ); - this.googleRecaptchaService.captchaVersion$.subscribe(res => { - this.captchaVersion = res; - }); - this.googleRecaptchaService.captchaMode$.subscribe(res => { - this.captchaMode = res; - }); this.configService.findByPropertyName('registration.verification.enabled').pipe( getFirstCompletedRemoteData(), map((res: RemoteData) => { @@ -104,9 +88,17 @@ export class RegisterEmailFormComponent implements OnInit { if (!this.form.invalid) { if (this.registrationVerification) { let token; - if (this.captchaVersion === 'v3') { + let captchaVersion; + let captchaMode; + this.googleRecaptchaService.captchaVersion$.subscribe(res => { + captchaVersion = res; + }); + this.googleRecaptchaService.captchaMode$.subscribe(res => { + captchaMode = res; + }); + if (captchaVersion === 'v3') { token = await this.googleRecaptchaService.getRecaptchaToken('register_email'); - } else if (this.captchaMode === 'checkbox') { + } else if (captchaMode === 'checkbox') { token = await this.googleRecaptchaService.getRecaptchaTokenResponse(); } else { token = tokenV2; diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index 1f780198f4..4075f7f050 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -1,7 +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 } from 'src/app/core/google-recaptcha/google-recaptcha.service'; +import { CAPTCHA_COOKIE, CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service'; /** * Cookie for has_agreed_end_user @@ -157,12 +157,15 @@ export const klaroConfiguration: any = { onlyOnce: true, }, { - name: 'google-recaptcha', + name: CAPTCHA_NAME, purposes: ['registration-password-recovery'], - required: true, + required: false, cookies: [ CAPTCHA_COOKIE ], + onAccept: `window.refreshCaptchaScript()`, + onDecline: `window.refreshCaptchaScript()`, + onInit: `window.refreshCaptchaScript()`, onlyOnce: true, } ], diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 7aa884866f..024a3ca54b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1264,7 +1264,7 @@ "cookies.consent.app.title.google-recaptcha": "Google reCaptcha", - "cookies.consent.app.description.google-recaptcha": "Allows us to track registration and password recovery data", + "cookies.consent.app.description.google-recaptcha": "We use google reCAPTCHA service during registration and password recovery", From 6d1d7c36110c6f7e26aa9462f0d53feb658864a0 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Mon, 22 Aug 2022 15:30:18 +0530 Subject: [PATCH 11/35] [UXP-10] property check fixed --- .../cookies/browser-klaro.service.spec.ts | 18 +++++++++++++ .../shared/cookies/browser-klaro.service.ts | 25 +++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/app/shared/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts index 2155fb1bad..7ed00013ba 100644 --- a/src/app/shared/cookies/browser-klaro.service.spec.ts +++ b/src/app/shared/cookies/browser-klaro.service.spec.ts @@ -11,8 +11,13 @@ import { CookieService } from '../../core/services/cookie.service'; import { getTestScheduler } from 'jasmine-marbles'; import { MetadataValue } from '../../core/shared/metadata.models'; import { cloneDeep } from 'lodash'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; describe('BrowserKlaroService', () => { + const recaptchaProp = 'registration.verification.enabled'; + const recaptchaValue = 'true'; let translateService; let ePersonService; let authService; @@ -20,6 +25,14 @@ describe('BrowserKlaroService', () => { let user; let service: BrowserKlaroService; + let configurationDataService: ConfigurationDataService; + const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: recaptchaProp, + values: values, + }), + }); let mockConfig; let appName; @@ -38,6 +51,7 @@ describe('BrowserKlaroService', () => { isAuthenticated: observableOf(true), getAuthenticatedUserFromStore: observableOf(user) }); + configurationDataService = createConfigSuccessSpy(recaptchaValue); 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: () => { @@ -63,6 +77,10 @@ describe('BrowserKlaroService', () => { { provide: CookieService, useValue: cookieService + }, + { + provide: ConfigurationDataService, + useValue: configurationDataService } ] }); diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index 638d465864..517a945eef 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -4,15 +4,18 @@ import { combineLatest as observableCombineLatest, Observable, of as observableO import { AuthService } from '../../core/auth/auth.service'; import { TranslateService } from '@ngx-translate/core'; import { environment } from '../../../environments/environment'; -import { switchMap, take } from 'rxjs/operators'; +import { catchError, switchMap, take } from 'rxjs/operators'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { KlaroService } from './klaro.service'; -import { hasValue, isNotEmpty } from '../empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; import { CookieService } from '../../core/services/cookie.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { cloneDeep, debounce } from 'lodash'; import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration'; import { Operation } from 'fast-json-patch'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service'; /** * Metadata field to store a user's cookie consent preferences in @@ -52,6 +55,7 @@ export class BrowserKlaroService extends KlaroService { private translateService: TranslateService, private authService: AuthService, private ePersonService: EPersonDataService, + private configService: ConfigurationDataService, private cookieService: CookieService) { super(); } @@ -68,6 +72,15 @@ export class BrowserKlaroService extends KlaroService { this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; } + this.configService.findByPropertyName('registration.verification.enabled').pipe( + getFirstCompletedRemoteData(), + catchError(this.removeGoogleRecaptcha()) + ).subscribe((remoteData) => { + // make sure we got a success response from the backend + if (!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() === 'false' ) { + this.removeGoogleRecaptcha(); + } + }); this.translateService.setDefaultLang(environment.defaultLanguage); const user$: Observable = this.getUser$(); @@ -257,4 +270,12 @@ export class BrowserKlaroService extends KlaroService { getStorageName(identifier: string) { return 'klaro-' + identifier; } + + /** + * remove the google recaptcha from the services + */ + removeGoogleRecaptcha() { + this.klaroConfig.services = klaroConfiguration.services.filter(config => config.name !== CAPTCHA_NAME); + return this.klaroConfig.services; + } } From b72b37a64706baba7f3ea28ec4909acf7befda56 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Mon, 22 Aug 2022 17:30:19 +0530 Subject: [PATCH 12/35] [UXP-10] remove configuration fixed --- .../register-email-form.component.ts | 2 +- src/app/shared/cookies/browser-klaro.service.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 0079ea2da8..08e172d5f1 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -107,7 +107,7 @@ export class RegisterEmailFormComponent implements OnInit { this.registration(token); } else { this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), - this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`, {email: this.email.value})); + this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`)); } } else { this.registration(); diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index 517a945eef..b763335b04 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -74,10 +74,10 @@ export class BrowserKlaroService extends KlaroService { this.configService.findByPropertyName('registration.verification.enabled').pipe( getFirstCompletedRemoteData(), - catchError(this.removeGoogleRecaptcha()) ).subscribe((remoteData) => { + this.klaroConfig = klaroConfiguration; // make sure we got a success response from the backend - if (!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() === 'false' ) { + if (!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() !== 'true') { this.removeGoogleRecaptcha(); } }); @@ -275,7 +275,8 @@ export class BrowserKlaroService extends KlaroService { * remove the google recaptcha from the services */ removeGoogleRecaptcha() { - this.klaroConfig.services = klaroConfiguration.services.filter(config => config.name !== CAPTCHA_NAME); - return this.klaroConfig.services; - } + this.klaroConfig.services = klaroConfiguration.services.filter(config => config.name !== CAPTCHA_NAME); + return this.klaroConfig.services; + } + } From b6d6091c87e814c0320b27f315edba1b925732e8 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 7 Sep 2022 18:02:23 +0200 Subject: [PATCH 13/35] [UXP-10] Code refactoring --- src/app/app.component.ts | 7 +- .../google-recaptcha.service.ts | 116 ++++++++++-------- .../register-email-form.component.html | 14 +-- .../register-email-form.component.ts | 81 ++++++------ 4 files changed, 115 insertions(+), 103 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 68c1bce6f3..e7033f51ba 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -15,7 +15,8 @@ import { ActivationEnd, NavigationCancel, NavigationEnd, - NavigationStart, ResolveEnd, + NavigationStart, + ResolveEnd, Router, } from '@angular/router'; @@ -48,8 +49,7 @@ import { BASE_THEME_NAME } from './shared/theme-support/theme.constants'; import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; import { getDefaultThemeConfig } from '../config/config.util'; -import { AppConfig, APP_CONFIG } from '../config/app-config.interface'; -import { GoogleRecaptchaService } from './core/google-recaptcha/google-recaptcha.service'; +import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; @Component({ selector: 'ds-app', @@ -110,7 +110,6 @@ export class AppComponent implements OnInit, AfterViewInit { private modalConfig: NgbModalConfig, @Optional() private cookiesService: KlaroService, @Optional() private googleAnalyticsService: GoogleAnalyticsService, - @Optional() private googleRecaptchaService: GoogleRecaptchaService, ) { if (!isEqual(environment, this.appConfig)) { diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts index 4f602ef575..080ddfc19f 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -5,8 +5,8 @@ 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, take } from 'rxjs/operators'; -import { combineLatest, Observable, of } from 'rxjs'; +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'; @@ -20,25 +20,33 @@ export const CAPTCHA_NAME = 'google-recaptcha'; export class GoogleRecaptchaService { private renderer: Renderer2; - /** - * A Google Recaptcha site key - */ - captchaSiteKeyStr: string; - - /** - * A Google Recaptcha site key - */ - captchaSiteKey$: Observable; - - /** - * A Google Recaptcha mode - */ - captchaMode$: Observable = of('invisible'); /** * A Google Recaptcha version */ - captchaVersion$: Observable = of(''); + private captchaVersionSubject$ = new BehaviorSubject(null); + + /** + * The Google Recaptcha Key + */ + private captchaKeySubject$ = new BehaviorSubject(null); + + /** + * The Google Recaptcha mode + */ + private captchaModeSubject$ = new BehaviorSubject(null); + + captchaKey(): Observable { + return this.captchaKeySubject$.asObservable(); + } + + captchaMode(): Observable { + return this.captchaModeSubject$.asObservable(); + } + + captchaVersion(): Observable { + return this.captchaVersionSubject$.asObservable(); + } constructor( private cookieService: CookieService, @@ -66,36 +74,46 @@ export class GoogleRecaptchaService { } loadRecaptchaProperties() { - const recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe( - take(1), + const recaptchaKeyRD$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe( getFirstCompletedRemoteData(), ); - const recaptchaVersion$ = this.configService.findByPropertyName('google.recaptcha.version').pipe( - take(1), + const recaptchaVersionRD$ = this.configService.findByPropertyName('google.recaptcha.version').pipe( getFirstCompletedRemoteData(), ); - const recaptchaMode$ = this.configService.findByPropertyName('google.recaptcha.mode').pipe( - take(1), + const recaptchaModeRD$ = this.configService.findByPropertyName('google.recaptcha.mode').pipe( getFirstCompletedRemoteData(), ); - combineLatest(recaptchaVersion$, recaptchaMode$, recaptchaKey$).subscribe(([recaptchaVersion, recaptchaMode, recaptchaKey]) => { - if (this.cookieService.get('klaro-anonymous') && this.cookieService.get('klaro-anonymous')[CAPTCHA_NAME]) { - if (recaptchaKey.hasSucceeded && isNotEmpty(recaptchaKey?.payload?.values[0])) { - this.captchaSiteKeyStr = recaptchaKey?.payload?.values[0]; - this.captchaSiteKey$ = of(recaptchaKey?.payload?.values[0]); + 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 = 'https://www.google.com/recaptcha/api.js'; + this.captchaModeSubject$.next(recaptchaModeRD.payload?.values[0]); + } + break; + default: + // TODO handle error } - if (recaptchaVersion.hasSucceeded && isNotEmpty(recaptchaVersion?.payload?.values[0]) && recaptchaVersion?.payload?.values[0] === 'v3') { - this.captchaVersion$ = of('v3'); - if (recaptchaKey.hasSucceeded && isNotEmpty(recaptchaKey?.payload?.values[0])) { - this.loadScript(this.buildCaptchaUrl(recaptchaKey?.payload?.values[0])); - } - } else { - this.captchaVersion$ = of('v2'); - const captchaUrl = 'https://www.google.com/recaptcha/api.js'; - if (recaptchaMode.hasSucceeded && isNotEmpty(recaptchaMode?.payload?.values[0])) { - this.captchaMode$ = of(recaptchaMode?.payload?.values[0]); - this.loadScript(captchaUrl); - } + if (captchaUrl) { + this.loadScript(captchaUrl); } } }); @@ -105,23 +123,21 @@ export class GoogleRecaptchaService { * Returns an observable of string * @param action action is the process type in which used to protect multiple spam REST calls */ - public async getRecaptchaToken (action) { - return await grecaptcha.execute(this.captchaSiteKeyStr, {action: action}); + public getRecaptchaToken(action) { + return this.captchaKey().pipe( + switchMap((key) => grecaptcha.execute(key, {action: action})) + ); } /** * Returns an observable of string */ - public async executeRecaptcha () { - return await grecaptcha.execute(); + public executeRecaptcha() { + return of(grecaptcha.execute()); } - /** - * Returns an observable of string - * @param action action is the process type in which used to protect multiple spam REST calls - */ - public async getRecaptchaTokenResponse () { - return await grecaptcha.getResponse(); + public getRecaptchaTokenResponse () { + return grecaptcha.getResponse(); } /** diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index b10833c097..19912ec0a9 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -24,23 +24,23 @@
{{MESSAGE_PREFIX + '.email.hint' |translate}}
-
- +
+
- - - + {{ MESSAGE_PREFIX + '.submit' | translate }} diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 08e172d5f1..084446eced 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -7,12 +7,12 @@ 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 { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { isNotEmpty } from '../shared/empty.util'; +import { combineLatest, Observable, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; -import { Observable } from 'rxjs'; @Component({ selector: 'ds-register-email-form', @@ -39,9 +39,16 @@ export class RegisterEmailFormComponent implements OnInit { */ registrationVerification = false; - recaptchaKey$: Observable; + captchaVersion(): Observable { + return this.googleRecaptchaService.captchaVersion(); + } - constructor( + captchaMode(): Observable { + return this.googleRecaptchaService.captchaMode(); + } + + +constructor( private epersonRegistrationService: EpersonRegistrationService, private notificationService: NotificationsService, private translateService: TranslateService, @@ -61,14 +68,9 @@ export class RegisterEmailFormComponent implements OnInit { ], }) }); - this.recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe( - getFirstSucceededRemoteDataPayload(), - ); this.configService.findByPropertyName('registration.verification.enabled').pipe( - getFirstCompletedRemoteData(), - map((res: RemoteData) => { - return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0].toLowerCase() === 'true'; - }) + getFirstSucceededRemoteDataPayload(), + map((res: ConfigurationProperty) => res?.values[0].toLowerCase() === 'true') ).subscribe((res: boolean) => { this.registrationVerification = res; }); @@ -77,38 +79,36 @@ export class RegisterEmailFormComponent implements OnInit { /** * execute the captcha function for v2 invisible */ - async executeRecaptcha() { - await this.googleRecaptchaService.executeRecaptcha(); + executeRecaptcha() { + console.log('executeRecaptcha'); + this.googleRecaptchaService.executeRecaptcha(); } /** * Register an email address */ - async register(tokenV2 = null) { + register(tokenV2 = null) { if (!this.form.invalid) { if (this.registrationVerification) { - let token; - let captchaVersion; - let captchaMode; - this.googleRecaptchaService.captchaVersion$.subscribe(res => { - captchaVersion = res; - }); - this.googleRecaptchaService.captchaMode$.subscribe(res => { - captchaMode = res; - }); - if (captchaVersion === 'v3') { - token = await this.googleRecaptchaService.getRecaptchaToken('register_email'); - } else if (captchaMode === 'checkbox') { - token = await this.googleRecaptchaService.getRecaptchaTokenResponse(); - } else { - token = tokenV2; - } - if (isNotEmpty(token)) { - this.registration(token); - } else { - this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), - this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`)); - } + combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( + switchMap(([captchaVersion, captchaMode]) => { + if (captchaVersion === 'v3') { + return this.googleRecaptchaService.getRecaptchaToken('register_email'); + } else if (captchaMode === 'checkbox') { + return this.googleRecaptchaService.getRecaptchaTokenResponse(); + } else { + return of(tokenV2); + } + }), + ).subscribe((token) => { + if (isNotEmpty(token)) { + this.registration(token); + } else { + this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), + this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`)); + } + } + ); } else { this.registration(); } @@ -119,12 +119,9 @@ export class RegisterEmailFormComponent implements OnInit { * Registration of an email address */ registration(captchaToken = null) { - let registerEmail$; - if (captchaToken) { - registerEmail$ = this.epersonRegistrationService.registerEmail(this.email.value, captchaToken); - } else { - registerEmail$ = this.epersonRegistrationService.registerEmail(this.email.value); - } + let registerEmail$ = captchaToken ? + this.epersonRegistrationService.registerEmail(this.email.value, captchaToken) : + this.epersonRegistrationService.registerEmail(this.email.value); registerEmail$.subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`), From db6c8f00a87728dabfee4e55f7e977b1e4e3e0f2 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 7 Sep 2022 19:01:54 +0200 Subject: [PATCH 14/35] [UXP-10] Fix headers --- src/app/core/data/eperson-registration.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 6f64879e0b..43a5ae8168 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -67,7 +67,7 @@ export class EpersonRegistrationService { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); if (captchaToken) { - headers = headers.append('X-Recaptcha-Token', captchaToken); + headers = headers.append('x-recaptcha-token', captchaToken); } options.headers = headers; From 0b7cf23e3fcc64c978c11a4dbd0f42be54d0cd05 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Thu, 8 Sep 2022 16:52:43 +0200 Subject: [PATCH 15/35] [UXP-10] Fix onInit script --- src/app/shared/cookies/browser-klaro.service.ts | 7 +++---- src/app/shared/cookies/klaro-configuration.ts | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index b763335b04..e6ae29e44c 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -4,10 +4,10 @@ import { combineLatest as observableCombineLatest, Observable, of as observableO import { AuthService } from '../../core/auth/auth.service'; import { TranslateService } from '@ngx-translate/core'; import { environment } from '../../../environments/environment'; -import { catchError, switchMap, take } from 'rxjs/operators'; +import { switchMap, take } from 'rxjs/operators'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { KlaroService } from './klaro.service'; -import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; +import { hasValue, isNotEmpty } from '../empty.util'; import { CookieService } from '../../core/services/cookie.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { cloneDeep, debounce } from 'lodash'; @@ -274,9 +274,8 @@ export class BrowserKlaroService extends KlaroService { /** * remove the google recaptcha from the services */ - removeGoogleRecaptcha() { + removeGoogleRecaptcha(): void { this.klaroConfig.services = klaroConfiguration.services.filter(config => config.name !== CAPTCHA_NAME); - return this.klaroConfig.services; } } diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index 4075f7f050..b4ed3d91ba 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -163,9 +163,8 @@ export const klaroConfiguration: any = { cookies: [ CAPTCHA_COOKIE ], - onAccept: `window.refreshCaptchaScript()`, - onDecline: `window.refreshCaptchaScript()`, - onInit: `window.refreshCaptchaScript()`, + onAccept: `window.refreshCaptchaScript?.call()`, + onDecline: `window.refreshCaptchaScript?.call()`, onlyOnce: true, } ], From 05784bfec642a9e8e440d33c6e57905a6dbe3949 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Thu, 8 Sep 2022 17:14:14 +0200 Subject: [PATCH 16/35] [UXP-10] Fix remove Recaptcha from klaro config --- src/app/shared/cookies/browser-klaro.service.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index e6ae29e44c..1481f7c0d3 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -7,7 +7,7 @@ import { environment } from '../../../environments/environment'; import { switchMap, take } from 'rxjs/operators'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { KlaroService } from './klaro.service'; -import { hasValue, isNotEmpty } from '../empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; import { CookieService } from '../../core/services/cookie.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { cloneDeep, debounce } from 'lodash'; @@ -75,10 +75,8 @@ export class BrowserKlaroService extends KlaroService { this.configService.findByPropertyName('registration.verification.enabled').pipe( getFirstCompletedRemoteData(), ).subscribe((remoteData) => { - this.klaroConfig = klaroConfiguration; - // make sure we got a success response from the backend - if (!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() !== 'true') { - this.removeGoogleRecaptcha(); + if (!remoteData.hasSucceeded || isEmpty(remoteData.payload?.values) || remoteData.payload.values[0].toLowerCase() !== 'true') { + this.klaroConfig.services = klaroConfiguration.services.filter(config => config.name !== CAPTCHA_NAME); } }); this.translateService.setDefaultLang(environment.defaultLanguage); @@ -271,11 +269,4 @@ export class BrowserKlaroService extends KlaroService { return 'klaro-' + identifier; } - /** - * remove the google recaptcha from the services - */ - removeGoogleRecaptcha(): void { - this.klaroConfig.services = klaroConfiguration.services.filter(config => config.name !== CAPTCHA_NAME); - } - } From 97a12cae47667ad876c1e30205469fa13c147d43 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Thu, 8 Sep 2022 20:06:36 +0200 Subject: [PATCH 17/35] [UXP-10] Notify unaccepted cookies - Fixes --- .../register-email-form.component.html | 36 +++++++++------- .../register-email-form.component.ts | 42 +++++++++++++++---- .../shared/cookies/browser-klaro.service.ts | 2 +- 3 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index 19912ec0a9..4def4d94a8 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -2,7 +2,7 @@

{{MESSAGE_PREFIX + '.header'|translate}}

{{MESSAGE_PREFIX + '.info' | translate}}

-
+
@@ -24,23 +24,31 @@
{{MESSAGE_PREFIX + '.email.hint' |translate}}
-
- -
- - + +

{{ MESSAGE_PREFIX + '.google-recaptcha.must-accept-cookies' | translate }}

+

{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}

+
+ +
+ +
+ + + + + + + + + diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 084446eced..14c22d7650 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { 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'; @@ -11,8 +11,11 @@ import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { isNotEmpty } from '../shared/empty.util'; import { combineLatest, Observable, of, switchMap } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; +import { map, take, tap } 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', @@ -34,6 +37,8 @@ export class RegisterEmailFormComponent implements OnInit { @Input() MESSAGE_PREFIX: string; + public AlertTypeEnum = AlertType; + /** * registration verification configuration */ @@ -47,15 +52,16 @@ export class RegisterEmailFormComponent implements OnInit { return this.googleRecaptchaService.captchaMode(); } - -constructor( + constructor( private epersonRegistrationService: EpersonRegistrationService, private notificationService: NotificationsService, private translateService: TranslateService, private router: Router, private formBuilder: FormBuilder, private configService: ConfigurationDataService, - public googleRecaptchaService: GoogleRecaptchaService + public googleRecaptchaService: GoogleRecaptchaService, + public cookieService: CookieService, + @Optional() public klaroService: KlaroService, ) { } @@ -80,14 +86,13 @@ constructor( * execute the captcha function for v2 invisible */ executeRecaptcha() { - console.log('executeRecaptcha'); this.googleRecaptchaService.executeRecaptcha(); } /** * Register an email address */ - register(tokenV2 = null) { + register(tokenV2?) { if (!this.form.invalid) { if (this.registrationVerification) { combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( @@ -100,6 +105,7 @@ constructor( return of(tokenV2); } }), + take(1), ).subscribe((token) => { if (isNotEmpty(token)) { this.registration(token); @@ -134,6 +140,26 @@ constructor( }); } + isRecaptchaCookieAccepted(): boolean { + const klaroAnonymousCookie = this.cookieService.get('klaro-anonymous'); + return isNotEmpty(klaroAnonymousCookie) ? klaroAnonymousCookie[CAPTCHA_NAME] : false; + } + + disableRegisterButton(): Observable { + return combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( + switchMap(([captchaVersion, captchaMode]) => { + if (captchaVersion === 'v2' && captchaMode === 'checkbox') { +// this.googleRecaptchaService.getRecaptchaTokenResponse() + return of(false); + // TODO disable if captcha unchecked + } else { + return of(false); + } + }), + tap(console.log) + ); + } + get email() { return this.form.get('email'); } diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index 1481f7c0d3..a1ed0ff77d 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -75,7 +75,7 @@ export class BrowserKlaroService extends KlaroService { this.configService.findByPropertyName('registration.verification.enabled').pipe( getFirstCompletedRemoteData(), ).subscribe((remoteData) => { - if (!remoteData.hasSucceeded || isEmpty(remoteData.payload?.values) || remoteData.payload.values[0].toLowerCase() !== 'true') { + if (remoteData.statusCode === 404 || isEmpty(remoteData.payload?.values) || remoteData.payload.values[0].toLowerCase() !== 'true') { this.klaroConfig.services = klaroConfiguration.services.filter(config => config.name !== CAPTCHA_NAME); } }); From 895e44a25fa7ed40e66fc32d37a2af07aeb69d81 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 9 Sep 2022 13:19:07 +0200 Subject: [PATCH 18/35] [UXP-10] Disable button - Error handling - Notifications --- .../register-email-form.component.html | 8 ++- .../register-email-form.component.ts | 64 +++++++++++++++---- .../google-recaptcha.component.html | 6 +- .../google-recaptcha.component.ts | 24 ++++++- src/assets/i18n/en.json5 | 10 ++- 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index 4def4d94a8..8ade052708 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -31,16 +31,18 @@ -

{{ MESSAGE_PREFIX + '.google-recaptcha.must-accept-cookies' | translate }}

+

{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}

- +
- diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 14c22d7650..3338200eb9 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit, Optional } 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'; @@ -10,7 +10,7 @@ import { ConfigurationDataService } from '../core/data/configuration-data.servic import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { isNotEmpty } from '../shared/empty.util'; -import { combineLatest, Observable, of, switchMap } from 'rxjs'; +import { BehaviorSubject, combineLatest, Observable, of, switchMap } from 'rxjs'; import { map, take, tap } from 'rxjs/operators'; import { CAPTCHA_NAME, GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; import { AlertType } from '../shared/alert/aletr-type'; @@ -44,6 +44,11 @@ export class RegisterEmailFormComponent implements OnInit { */ registrationVerification = false; + /** + * Return true if the user completed the reCaptcha verification (checkbox mode) + */ + checkboxCheckedSubject$ = new BehaviorSubject(false); + captchaVersion(): Observable { return this.googleRecaptchaService.captchaVersion(); } @@ -62,6 +67,8 @@ export class RegisterEmailFormComponent implements OnInit { public googleRecaptchaService: GoogleRecaptchaService, public cookieService: CookieService, @Optional() public klaroService: KlaroService, + private changeDetectorRef: ChangeDetectorRef, + private notificationsService: NotificationsService, ) { } @@ -99,10 +106,13 @@ export class RegisterEmailFormComponent implements OnInit { switchMap(([captchaVersion, captchaMode]) => { if (captchaVersion === 'v3') { return this.googleRecaptchaService.getRecaptchaToken('register_email'); - } else if (captchaMode === 'checkbox') { + } else if (captchaVersion === 'v2' && captchaMode === 'checkbox') { return this.googleRecaptchaService.getRecaptchaTokenResponse(); - } else { + } else if (captchaVersion === 'v2' && captchaMode === 'invisible') { return of(tokenV2); + } else { + console.error(`Invalid reCaptcha configuration: version = ${captchaVersion}, mode = ${captchaMode}`); + this.showNotification('error'); } }), take(1), @@ -110,8 +120,8 @@ export class RegisterEmailFormComponent implements OnInit { if (isNotEmpty(token)) { this.registration(token); } else { - this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), - this.translateService.get(`${this.MESSAGE_PREFIX}.error.recaptcha`)); + console.error('reCaptcha error'); + this.showNotification('error'); } } ); @@ -140,23 +150,28 @@ export class RegisterEmailFormComponent implements OnInit { }); } + /** + * 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; } - disableRegisterButton(): Observable { + /** + * Return true if the user completed the reCaptcha verification (checkbox mode) + */ + isCheckboxChecked(): Observable { return combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( switchMap(([captchaVersion, captchaMode]) => { if (captchaVersion === 'v2' && captchaMode === 'checkbox') { -// this.googleRecaptchaService.getRecaptchaTokenResponse() - return of(false); - // TODO disable if captcha unchecked + return this.checkboxCheckedSubject$.asObservable(); } else { - return of(false); + return of(true); } }), - tap(console.log) + tap(console.log), + tap(() => { this.changeDetectorRef.markForCheck(); }) ); } @@ -164,4 +179,29 @@ export class RegisterEmailFormComponent implements OnInit { return this.form.get('email'); } + onCheckboxChecked($event) { + if (isNotEmpty($event)) { + this.checkboxCheckedSubject$.next(true); + } + } + + /** + * 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: + } + } + } diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.html b/src/app/shared/google-recaptcha/google-recaptcha.component.html index 50bc51c808..315514d696 100644 --- a/src/app/shared/google-recaptcha/google-recaptcha.component.html +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.html @@ -1,4 +1,6 @@
\ No newline at end of file + [attr.data-size]="captchaMode === 'invisible' ? 'invisible' : null"> diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.ts b/src/app/shared/google-recaptcha/google-recaptcha.component.ts index f9c9b599f6..5b08e770db 100644 --- a/src/app/shared/google-recaptcha/google-recaptcha.component.ts +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.ts @@ -13,11 +13,16 @@ import { NativeWindowRef, NativeWindowService } from 'src/app/core/services/wind export class GoogleRecaptchaComponent implements OnInit { @Input() captchaMode: string; + /** * An EventEmitter that's fired whenever the form is being submitted */ @Output() executeRecaptcha: EventEmitter = new EventEmitter(); + @Output() checkboxChecked: EventEmitter = new EventEmitter(); + + @Output() showNotification: EventEmitter = new EventEmitter(); + recaptchaKey$: Observable; constructor( @@ -34,12 +39,27 @@ export class GoogleRecaptchaComponent implements OnInit { getFirstSucceededRemoteDataPayload(), ); if (this.captchaMode === 'invisible') { - this._window.nativeWindow.executeRecaptcha = this.execute; + this._window.nativeWindow.executeRecaptchaCallback = this.executeRecaptchaFcn; } + if (this.captchaMode === 'checkbox') { + this._window.nativeWindow.checkboxCheckedCallback = this.checkboxCheckedFcn; + } + this._window.nativeWindow.expiredCallback = this.notificationFcn('expired'); + this._window.nativeWindow.errorCallback = this.notificationFcn('error'); } - execute = (event) => { + executeRecaptchaFcn = (event) => { this.executeRecaptcha.emit(event); }; + checkboxCheckedFcn = (event) => { + this.checkboxChecked.emit(event); // todo fix con boolean + }; + + notificationFcn(key) { + return () => { + this.showNotification.emit(key); + }; + } + } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 024a3ca54b..ce0e4e0fa0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -640,7 +640,7 @@ "bitstream-request-a-copy.alert.canDownload2": "here", "bitstream-request-a-copy.header": "Request a copy of the file", - + "bitstream-request-a-copy.intro": "Enter the following information to request a copy for the following item: ", "bitstream-request-a-copy.intro.bitstream.one": "Requesting the following file: ", @@ -3206,7 +3206,15 @@ "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 Registration and Password recovery (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", From 8af725e76f8f66cf58023f63fe0f5b3033e892c9 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 9 Sep 2022 16:32:23 +0200 Subject: [PATCH 19/35] [UXP-10] Refactoring WIP --- .../register-email-form.component.html | 5 +-- .../register-email-form.component.ts | 32 +++++++++---------- .../google-recaptcha.component.html | 2 +- .../google-recaptcha.component.ts | 27 +++++++++------- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index 8ade052708..63320db9d3 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -35,16 +35,17 @@

{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}

-
+
- + invalid = {{form.invalid}}, disableUntilChecked = {{disableUntilChecked() | async}} diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 3338200eb9..dc82cec86a 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -11,7 +11,7 @@ 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, take, tap } from 'rxjs/operators'; +import { map, startWith, take, tap } 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'; @@ -159,19 +159,18 @@ export class RegisterEmailFormComponent implements OnInit { } /** - * Return true if the user completed the reCaptcha verification (checkbox mode) + * Return true if the user has not completed the reCaptcha verification (checkbox mode) */ - isCheckboxChecked(): Observable { - return combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( - switchMap(([captchaVersion, captchaMode]) => { - if (captchaVersion === 'v2' && captchaMode === 'checkbox') { - return this.checkboxCheckedSubject$.asObservable(); - } else { - return of(true); - } - }), - tap(console.log), - tap(() => { this.changeDetectorRef.markForCheck(); }) + disableUntilChecked(): Observable { + 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), + tap((res) => { + console.log('DISABLED = ' + res); + }), // TODO remove + // tap(() => { this.changeDetectorRef.markForCheck(); }), ); } @@ -179,10 +178,8 @@ export class RegisterEmailFormComponent implements OnInit { return this.form.get('email'); } - onCheckboxChecked($event) { - if (isNotEmpty($event)) { - this.checkboxCheckedSubject$.next(true); - } + onCheckboxChecked(checked: boolean) { + this.checkboxCheckedSubject$.next(checked); } /** @@ -201,6 +198,7 @@ export class RegisterEmailFormComponent implements OnInit { this.notificationsService.error(notificationTitle, notificationErrorMsg); break; default: + console.warn(`Unimplemented notification '${key}' from reCaptcha service`); } } diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.html b/src/app/shared/google-recaptcha/google-recaptcha.component.html index 315514d696..64c05cb739 100644 --- a/src/app/shared/google-recaptcha/google-recaptcha.component.html +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.html @@ -1,5 +1,5 @@
{ - this.executeRecaptcha.emit(event); - }; - - checkboxCheckedFcn = (event) => { - this.checkboxChecked.emit(event); // todo fix con boolean + dataCallbackFcn = ($event) => { + switch (this.captchaMode) { + case 'invisible': + this.executeRecaptcha.emit($event); + break; + case 'checkbox': + console.log('CB ' + isNotEmpty($event)); + this.checkboxChecked.emit(isNotEmpty($event)); // todo fix con boolean + break; + default: + console.error(`Invalid reCaptcha mode '${this.captchaMode}`); + this.showNotification.emit('error'); + } }; notificationFcn(key) { From 6dfc5ef2f5e95c7965ec25fb1772be0b2d6ea850 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 9 Sep 2022 17:04:01 +0200 Subject: [PATCH 20/35] [UXP-10] Handle captcha expiration --- .../google-recaptcha.component.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.ts b/src/app/shared/google-recaptcha/google-recaptcha.component.ts index 265ee96e02..6c3bb85808 100644 --- a/src/app/shared/google-recaptcha/google-recaptcha.component.ts +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.ts @@ -40,8 +40,8 @@ export class GoogleRecaptchaComponent implements OnInit { getFirstSucceededRemoteDataPayload(), ); this._window.nativeWindow.dataCallback = this.dataCallbackFcn; - this._window.nativeWindow.expiredCallback = this.notificationFcn('expired'); - this._window.nativeWindow.errorCallback = this.notificationFcn('error'); + this._window.nativeWindow.expiredCallback = this.expiredCallbackFcn; + this._window.nativeWindow.errorCallback = this.errorCallbackFcn; } dataCallbackFcn = ($event) => { @@ -59,10 +59,13 @@ export class GoogleRecaptchaComponent implements OnInit { } }; - notificationFcn(key) { - return () => { - this.showNotification.emit(key); - }; - } + expiredCallbackFcn = () => { + this.checkboxChecked.emit(false); + this.showNotification.emit('expired'); + }; + + errorCallbackFcn = () => { + this.showNotification.emit('error'); + }; } From 89a73208232bfa87bdfc06d73f8fbaea0ff19f56 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 9 Sep 2022 17:45:46 +0200 Subject: [PATCH 21/35] [UXP-10] Refactoring - Disable button fix --- .../register-email-form.component.html | 6 +++--- .../register-email-form.component.ts | 16 ++++++++++------ .../google-recaptcha.component.ts | 1 - 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index 63320db9d3..323fdb4151 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -28,7 +28,6 @@
-

@@ -42,10 +41,9 @@ - - invalid = {{form.invalid}}, disableUntilChecked = {{disableUntilChecked() | async}} @@ -53,5 +51,7 @@ {{ MESSAGE_PREFIX + '.submit' | translate }} + + diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index dc82cec86a..023e2c7d98 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -11,7 +11,7 @@ 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, tap } from 'rxjs/operators'; +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'; @@ -49,6 +49,8 @@ export class RegisterEmailFormComponent implements OnInit { */ checkboxCheckedSubject$ = new BehaviorSubject(false); + disableUntilChecked = true; + captchaVersion(): Observable { return this.googleRecaptchaService.captchaVersion(); } @@ -87,6 +89,12 @@ export class RegisterEmailFormComponent implements OnInit { ).subscribe((res: boolean) => { this.registrationVerification = res; }); + + this.disableUntilCheckedFcn().subscribe((res) => { + this.disableUntilChecked = res; + this.changeDetectorRef.detectChanges(); + }); + } /** @@ -161,16 +169,12 @@ export class RegisterEmailFormComponent implements OnInit { /** * Return true if the user has not completed the reCaptcha verification (checkbox mode) */ - disableUntilChecked(): Observable { + disableUntilCheckedFcn(): Observable { 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), - tap((res) => { - console.log('DISABLED = ' + res); - }), // TODO remove - // tap(() => { this.changeDetectorRef.markForCheck(); }), ); } diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.ts b/src/app/shared/google-recaptcha/google-recaptcha.component.ts index 6c3bb85808..980699046b 100644 --- a/src/app/shared/google-recaptcha/google-recaptcha.component.ts +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.ts @@ -50,7 +50,6 @@ export class GoogleRecaptchaComponent implements OnInit { this.executeRecaptcha.emit($event); break; case 'checkbox': - console.log('CB ' + isNotEmpty($event)); this.checkboxChecked.emit(isNotEmpty($event)); // todo fix con boolean break; default: From e57970349d146378b9b0a68980299ef14e72323d Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 9 Sep 2022 18:22:54 +0200 Subject: [PATCH 22/35] [UXP-10] Test fixed --- .../data/eperson-registration.service.spec.ts | 2 +- .../register-email-form.component.spec.ts | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index c7785302ef..afd4927103 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -94,7 +94,7 @@ describe('EpersonRegistrationService', () => { const expected = service.registerEmail('test@mail.org', 'afreshcaptchatoken'); let headers = new HttpHeaders(); const options: HttpOptions = Object.create({}); - headers = headers.append('X-Recaptcha-Token', 'afreshcaptchatoken'); + headers = headers.append('x-recaptcha-token', 'afreshcaptchatoken'); options.headers = headers; expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration, options)); diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index 55004c044b..bac922c73b 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -16,6 +16,8 @@ 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', () => { @@ -30,17 +32,18 @@ describe('RegisterEmailComponent', () => { findByPropertyName: jasmine.createSpy('findByPropertyName') }); - const googleRecaptchaService = jasmine.createSpyObj('googleRecaptchaService', { - getRecaptchaToken: Promise.resolve('googleRecaptchaToken'), - executeRecaptcha: Promise.resolve('googleRecaptchaToken'), - getRecaptchaTokenResponse: Promise.resolve('googleRecaptchaToken') - }); - 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(); @@ -59,6 +62,7 @@ describe('RegisterEmailComponent', () => { {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] From b02826d24d4c4e5b8ee168a475d620be086e63c5 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 9 Sep 2022 18:51:58 +0200 Subject: [PATCH 23/35] [UXP-10] Removed unused import --- src/app/core/google-recaptcha/google-recaptcha.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/google-recaptcha/google-recaptcha.module.ts b/src/app/core/google-recaptcha/google-recaptcha.module.ts index 8af9adb641..64620a48f4 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.module.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.module.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { GoogleRecaptchaComponent } from '../../shared/google-recaptcha/google-recaptcha.component'; -import { SharedModule } from '../../shared/shared.module'; import { GoogleRecaptchaService } from './google-recaptcha.service'; From 183653112e41fd07251f862a889f82d228a11642 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Thu, 15 Sep 2022 10:40:59 +0200 Subject: [PATCH 24/35] [UXP-10] e2e test fixed --- cypress/support/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 024b46cdde..2d4f6d8fd3 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -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%22registration-password-recovery%22:true}'); }); // For better stability between tests, we visit "about:blank" (i.e. blank page) after each test. From 70f2625d15dd43ac6868eee02516a414b5fbf6b2 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Thu, 15 Sep 2022 13:57:06 +0200 Subject: [PATCH 25/35] [UXP-10] e2e test fix --- cypress/support/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 2d4f6d8fd3..70da23f044 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -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%2C%22registration-password-recovery%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. From 4484c3ebbc9139abc2fe2458947803c58166785b Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 19 Sep 2022 16:26:31 +0200 Subject: [PATCH 26/35] [CST-6782] Fixed failing tests --- .../cookies/browser-klaro.service.spec.ts | 63 +++++++++++++++---- .../shared/cookies/browser-klaro.service.ts | 4 +- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/app/shared/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts index df4ac7ebd2..d65b1477d7 100644 --- a/src/app/shared/cookies/browser-klaro.service.spec.ts +++ b/src/app/shared/cookies/browser-klaro.service.spec.ts @@ -262,8 +262,10 @@ 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); + 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'); @@ -292,27 +294,64 @@ describe('BrowserKlaroService', () => { 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})); }); diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index 32a8258ab3..8932b8be0e 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -50,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'; /** @@ -90,7 +92,7 @@ export class BrowserKlaroService extends KlaroService { }), ); - this.configService.findByPropertyName('registration.verification.enabled').pipe( + this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe( getFirstCompletedRemoteData(), ).subscribe((remoteData) => { if (remoteData.statusCode === 404 || isEmpty(remoteData.payload?.values) || remoteData.payload.values[0].toLowerCase() !== 'true') { From 711982d423ca3623f77dd6767fdaa5da9b55d19e Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 23 Sep 2022 10:32:50 +0200 Subject: [PATCH 27/35] [CST-6782] fixes after merge --- src/assets/i18n/en.json5 | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index b9b0a178e6..8755cf3592 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1276,14 +1276,6 @@ "cookies.consent.purpose.sharing": "Sharing", - "cris-layout.toggle.open": "Open section", - - "cris-layout.toggle.close": "Close section", - - "cris-layout.toggle.aria.open": "Expand {{sectionHeader}} section", - - "cris-layout.toggle.aria.close": "Collapse {{sectionHeader}} section", - "curation-task.task.citationpage.label": "Generate Citation Page", "curation-task.task.checklinks.label": "Check Links in Metadata", From 11eacc10f740fbeaebdf3c32610c6b045d050453 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 23 Sep 2022 12:46:16 +0200 Subject: [PATCH 28/35] [CST-6782] Fix klaro configuration --- src/app/shared/cookies/klaro-configuration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index b4ed3d91ba..44869ee470 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -161,6 +161,7 @@ export const klaroConfiguration: any = { purposes: ['registration-password-recovery'], required: false, cookies: [ + [/^klaro-.+$/], CAPTCHA_COOKIE ], onAccept: `window.refreshCaptchaScript?.call()`, From 7ad68530ea4e251a1558b3e8903ddb1302d665af Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Mon, 26 Sep 2022 15:40:24 +0200 Subject: [PATCH 29/35] [CST-6782] JSdoc fixed; buildCaptchaUrl refactored; import cleanup --- src/app/core/data/eperson-registration.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 26ebc2d017..e3af6e2821 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -3,10 +3,10 @@ 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'; @@ -53,6 +53,7 @@ export class EpersonRegistrationService { /** * Register a new email address * @param email + * @param captchaToken */ registerEmail(email: string, captchaToken: string = null): Observable> { const registration = new Registration(); From ab3b05b950a0ad8ea7ca2c31232d6053d77aabab Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Mon, 26 Sep 2022 15:43:47 +0200 Subject: [PATCH 30/35] [CST-6782] JSdoc fixed; buildCaptchaUrl refactored; import cleanup --- src/app/core/google-recaptcha/google-recaptcha.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts index 080ddfc19f..c2e3fdc86f 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -105,7 +105,7 @@ export class GoogleRecaptchaService { break; case 'v2': if (recaptchaModeRD.hasSucceeded && isNotEmpty(recaptchaModeRD.payload?.values)) { - captchaUrl = 'https://www.google.com/recaptcha/api.js'; + captchaUrl = this.buildCaptchaUrl(); this.captchaModeSubject$.next(recaptchaModeRD.payload?.values[0]); } break; @@ -146,8 +146,9 @@ export class GoogleRecaptchaService { * @param key contains a secret key of a google captchas * @returns string which has google captcha url with google captchas key */ - buildCaptchaUrl(key: string) { - return `https://www.google.com/recaptcha/api.js?render=${key}`; + buildCaptchaUrl(key?: string) { + const apiUrl = 'https://www.google.com/recaptcha/api.js'; + return key ? `${apiUrl}?render=${key}` : apiUrl; } /** From 2f13beac7c94f49924f085d1f5a6da9bb0e3bfbe Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Mon, 26 Sep 2022 15:44:38 +0200 Subject: [PATCH 31/35] [CST-6782] JSdoc fixed --- src/app/core/data/eperson-registration.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index e3af6e2821..bfbecdaecb 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -53,7 +53,7 @@ export class EpersonRegistrationService { /** * Register a new email address * @param email - * @param captchaToken + * @param captchaToken the value of x-recaptcha-token header */ registerEmail(email: string, captchaToken: string = null): Observable> { const registration = new Registration(); From c69934aab6519ecda4aa184a03bd7ebce1e5a6b1 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Thu, 29 Sep 2022 22:07:15 +0200 Subject: [PATCH 32/35] [CST-6782] Fix --- src/app/core/google-recaptcha/google-recaptcha.service.ts | 2 +- src/app/register-email-form/register-email-form.component.ts | 2 +- src/app/shared/google-recaptcha/google-recaptcha.component.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts index c2e3fdc86f..72de1bb26c 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -136,7 +136,7 @@ export class GoogleRecaptchaService { return of(grecaptcha.execute()); } - public getRecaptchaTokenResponse () { + public getRecaptchaTokenResponse() { return grecaptcha.getResponse(); } diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 023e2c7d98..ced87b9e75 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -115,7 +115,7 @@ export class RegisterEmailFormComponent implements OnInit { if (captchaVersion === 'v3') { return this.googleRecaptchaService.getRecaptchaToken('register_email'); } else if (captchaVersion === 'v2' && captchaMode === 'checkbox') { - return this.googleRecaptchaService.getRecaptchaTokenResponse(); + return of(this.googleRecaptchaService.getRecaptchaTokenResponse()); } else if (captchaVersion === 'v2' && captchaMode === 'invisible') { return of(tokenV2); } else { diff --git a/src/app/shared/google-recaptcha/google-recaptcha.component.ts b/src/app/shared/google-recaptcha/google-recaptcha.component.ts index 980699046b..16c49ba45b 100644 --- a/src/app/shared/google-recaptcha/google-recaptcha.component.ts +++ b/src/app/shared/google-recaptcha/google-recaptcha.component.ts @@ -20,7 +20,7 @@ export class GoogleRecaptchaComponent implements OnInit { */ @Output() executeRecaptcha: EventEmitter = new EventEmitter(); - @Output() checkboxChecked: EventEmitter = new EventEmitter(); + @Output() checkboxChecked: EventEmitter = new EventEmitter(); @Output() showNotification: EventEmitter = new EventEmitter(); @@ -50,7 +50,7 @@ export class GoogleRecaptchaComponent implements OnInit { this.executeRecaptcha.emit($event); break; case 'checkbox': - this.checkboxChecked.emit(isNotEmpty($event)); // todo fix con boolean + this.checkboxChecked.emit(isNotEmpty($event)); break; default: console.error(`Invalid reCaptcha mode '${this.captchaMode}`); From 89cdad42d2efd016083740851bf8bfbdd04267bf Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Mon, 3 Oct 2022 17:34:00 +0200 Subject: [PATCH 33/35] [CST-6782] Hide missing cookie alert when verification is not enabled --- src/app/register-email-form/register-email-form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index 323fdb4151..6ae893d81c 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -29,7 +29,7 @@ - +

{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}

From 2e4b96b2dd00f8ca43f6b72ac1a6607c03e4b958 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Mon, 3 Oct 2022 17:41:38 +0200 Subject: [PATCH 34/35] [CST-6782] Disable registration button if cookies haven't been accepted --- src/app/register-email-form/register-email-form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index 6ae893d81c..cc0ce4c782 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -41,7 +41,7 @@ - From 3b7a830ffea68993aaf168f60dc6d78f3621ef69 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Tue, 4 Oct 2022 15:37:04 +0200 Subject: [PATCH 35/35] [CST-6782] Hide cookie settings when verification is disabled --- .../shared/cookies/browser-klaro.service.ts | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index cc0f26ff84..c6819012d9 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -81,24 +81,31 @@ export class BrowserKlaroService extends KlaroService { this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; } - const servicesToHide$: Observable = 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)), ); - this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe( + const hideRegistrationVerification$ = this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe( getFirstCompletedRemoteData(), - ).subscribe((remoteData) => { - if (remoteData.statusCode === 404 || isEmpty(remoteData.payload?.values) || remoteData.payload.values[0].toLowerCase() !== 'true') { - this.klaroConfig.services = klaroConfiguration.services.filter(config => config.name !== CAPTCHA_NAME); - } - }); + map((remoteData) => + !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() !== 'true' + ), + ); + + const servicesToHide$: Observable = 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);