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"