diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index bfbecdaecb..499d05af38 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -14,6 +14,7 @@ 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'; +import { HttpParams } from '@angular/common/http'; @Injectable({ providedIn: 'root', @@ -55,7 +56,7 @@ export class EpersonRegistrationService { * @param email * @param captchaToken the value of x-recaptcha-token header */ - registerEmail(email: string, captchaToken: string = null): Observable> { + registerEmail(email: string, captchaToken: string = null, type?: string): Observable> { const registration = new Registration(); registration.email = email; @@ -70,6 +71,11 @@ export class EpersonRegistrationService { } options.headers = headers; + if (hasValue(type)) { + options.params = type ? + new HttpParams({ fromString: 'accountRequestType=' + type }) : new HttpParams(); + } + href$.pipe( find((href: string) => hasValue(href)), map((href: string) => { diff --git a/src/app/forgot-password/forgot-password-email/forgot-email.component.html b/src/app/forgot-password/forgot-password-email/forgot-email.component.html index 263f142c2e..995108cdbc 100644 --- a/src/app/forgot-password/forgot-password-email/forgot-email.component.html +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.html @@ -1,3 +1,3 @@ - \ No newline at end of file + [MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest"> + diff --git a/src/app/forgot-password/forgot-password-email/forgot-email.component.ts b/src/app/forgot-password/forgot-password-email/forgot-email.component.ts index af482bdb67..66a61ed7ee 100644 --- a/src/app/forgot-password/forgot-password-email/forgot-email.component.ts +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { TYPE_REQUEST_FORGOT } from '../../register-email-form/register-email-form.component'; @Component({ selector: 'ds-forgot-email', @@ -9,5 +10,5 @@ import { Component } from '@angular/core'; * Component responsible the forgot password email step */ export class ForgotEmailComponent { - + typeRequest = TYPE_REQUEST_FORGOT; } 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 cc0ce4c782..ed79b1d2d1 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -2,6 +2,10 @@

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

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

+

+ {{ MESSAGE_PREFIX + '.info.maildomain' | translate}} {{ validMailDomains.join(', ')}} +

+
@@ -16,8 +20,11 @@ {{ MESSAGE_PREFIX + '.email.error.required' | translate }} - - {{ MESSAGE_PREFIX + '.email.error.pattern' | translate }} + + {{ MESSAGE_PREFIX + '.email.error.not-email-form' | translate }} + + {{ MESSAGE_PREFIX + '.email.error.not-valid-domain' | translate: { domains: validMailDomains.join(', ') } }} +
@@ -53,5 +60,4 @@
- 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 cf3b4b13d2..6136db4aec 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 @@ -12,14 +12,19 @@ import { EpersonRegistrationService } from '../core/data/eperson-registration.se import { By } from '@angular/platform-browser'; import { RouterStub } from '../shared/testing/router.stub'; import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub'; -import { RegisterEmailFormComponent } from './register-email-form.component'; +import { + RegisterEmailFormComponent, + TYPE_REQUEST_REGISTER, + TYPE_REQUEST_FORGOT +} 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'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; -describe('RegisterEmailComponent', () => { +describe('RegisterEmailFormComponent', () => { let comp: RegisterEmailFormComponent; let fixture: ComponentFixture; @@ -53,6 +58,8 @@ describe('RegisterEmailComponent', () => { registerEmail: createSuccessfulRemoteDataObject$({}) }); + jasmine.getEnv().allowRespy(true); + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule], declarations: [RegisterEmailFormComponent], @@ -95,17 +102,53 @@ describe('RegisterEmailComponent', () => { comp.form.patchValue({email: 'valid@email.org'}); expect(comp.form.invalid).toBeFalse(); }); + it('should accept email with other domain names on TYPE_REQUEST_FORGOT form', () => { + spyOn(configurationDataService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'authentication-password.domain.valid', + values: ['marvel.com'], + }))); + comp.typeRequest = TYPE_REQUEST_FORGOT; + + comp.ngOnInit(); + + comp.form.patchValue({ email: 'valid@email.org' }); + expect(comp.form.invalid).toBeFalse(); + }); it('should be valid when uppercase letters are used', () => { comp.form.patchValue({email: 'VALID@email.org'}); expect(comp.form.invalid).toBeFalse(); }); + it('should not accept email with other domain names', () => { + spyOn(configurationDataService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'authentication-password.domain.valid', + values: ['marvel.com'], + }))); + comp.typeRequest = TYPE_REQUEST_REGISTER; + + comp.ngOnInit(); + + comp.form.patchValue({ email: 'valid@email.org' }); + expect(comp.form.invalid).toBeTrue(); + }); + it('should accept email with the given domain name', () => { + spyOn(configurationDataService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'authentication-password.domain.valid', + values: ['marvel.com'], + }))); + comp.typeRequest = TYPE_REQUEST_REGISTER; + + comp.ngOnInit(); + + comp.form.patchValue({ email: 'thor.odinson@marvel.com' }); + expect(comp.form.invalid).toBeFalse(); + }); }); describe('register', () => { 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'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null, null); expect(notificationsService.success).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith(['/home']); }); @@ -115,7 +158,7 @@ describe('RegisterEmailComponent', () => { comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null, null); expect(notificationsService.error).toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled(); }); @@ -133,7 +176,7 @@ describe('RegisterEmailComponent', () => { comp.form.patchValue({email: 'valid@email.org'}); comp.register(); tick(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken', null); expect(notificationsService.success).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith(['/home']); })); @@ -144,7 +187,7 @@ describe('RegisterEmailComponent', () => { comp.register(); tick(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken', 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 561bd53e67..9f0b186d39 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -1,21 +1,25 @@ -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'; -import { Router } from '@angular/router'; -import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; -import { Registration } from '../core/shared/registration.model'; -import { RemoteData } from '../core/data/remote-data'; -import { ConfigurationDataService } from '../core/data/configuration-data.service'; -import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; -import { ConfigurationProperty } from '../core/shared/configuration-property.model'; -import { isNotEmpty } from '../shared/empty.util'; -import { BehaviorSubject, combineLatest, Observable, of, switchMap } from 'rxjs'; -import { map, startWith, take } from 'rxjs/operators'; -import { CAPTCHA_NAME, GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service'; -import { AlertType } from '../shared/alert/aletr-type'; -import { KlaroService } from '../shared/cookies/klaro.service'; -import { CookieService } from '../core/services/cookie.service'; +import { ChangeDetectorRef, Component, Input, OnDestroy, 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'; +import {Router} from '@angular/router'; +import { FormBuilder, FormControl, FormGroup, Validators, ValidatorFn } 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 { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import {ConfigurationProperty} from '../core/shared/configuration-property.model'; +import {isNotEmpty} from '../shared/empty.util'; +import {BehaviorSubject, combineLatest, Observable, of, switchMap} from 'rxjs'; +import {map, startWith, take} from 'rxjs/operators'; +import {CAPTCHA_NAME, GoogleRecaptchaService} from '../core/google-recaptcha/google-recaptcha.service'; +import {AlertType} from '../shared/alert/aletr-type'; +import {KlaroService} from '../shared/cookies/klaro.service'; +import {CookieService} from '../core/services/cookie.service'; +import { Subscription } from 'rxjs'; + +export const TYPE_REQUEST_FORGOT = 'forgot'; +export const TYPE_REQUEST_REGISTER = 'register'; @Component({ selector: 'ds-register-email-form', @@ -24,7 +28,7 @@ import { CookieService } from '../core/services/cookie.service'; /** * Component responsible to render an email registration form. */ -export class RegisterEmailFormComponent implements OnInit { +export class RegisterEmailFormComponent implements OnDestroy, OnInit { /** * The form containing the mail address @@ -37,6 +41,12 @@ export class RegisterEmailFormComponent implements OnInit { @Input() MESSAGE_PREFIX: string; + /** + * Type of register request to be done, register new email or forgot password (same endpoint) + */ + @Input() + typeRequest: string = null; + public AlertTypeEnum = AlertType; /** @@ -51,6 +61,11 @@ export class RegisterEmailFormComponent implements OnInit { disableUntilChecked = true; + validMailDomains: string[]; + TYPE_REQUEST_REGISTER = TYPE_REQUEST_REGISTER; + + subscriptions: Subscription[] = []; + captchaVersion(): Observable { return this.googleRecaptchaService.captchaVersion(); } @@ -72,31 +87,54 @@ export class RegisterEmailFormComponent implements OnInit { private changeDetectorRef: ChangeDetectorRef, private notificationsService: NotificationsService, ) { + } + ngOnDestroy(): void { + this.subscriptions.forEach((sub: Subscription) => sub.unsubscribe()); } ngOnInit(): void { + const validators: ValidatorFn[] = [ + Validators.required, + Validators.email, + // Regex pattern borrowed from HTML5 specs for a valid email address: + // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + Validators.pattern('^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$') + ]; this.form = this.formBuilder.group({ email: new FormControl('', { - validators: [Validators.required, - // Regex pattern borrowed from HTML5 specs for a valid email address: - // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address - Validators.pattern('^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$') - ], + validators: validators, }) }); - this.configService.findByPropertyName('registration.verification.enabled').pipe( + this.validMailDomains = []; + if (this.typeRequest === TYPE_REQUEST_REGISTER) { + this.subscriptions.push(this.configService.findByPropertyName('authentication-password.domain.valid') + .pipe(getAllSucceededRemoteDataPayload()) + .subscribe((remoteData: ConfigurationProperty) => { + this.validMailDomains = remoteData.values; + for (const remoteValue of remoteData.values) { + if (this.validMailDomains.length !== 0) { + this.form.get('email').setValidators([ + ...validators, + Validators.pattern(this.validMailDomains.map((domain: string) => '(^.*' + domain.replace(new RegExp('\\.', 'g'), '\\.') + '$)').join('|')), + ]); + this.form.updateValueAndValidity(); + } + } + this.changeDetectorRef.detectChanges(); + })); + } + this.subscriptions.push(this.configService.findByPropertyName('registration.verification.enabled').pipe( getFirstSucceededRemoteDataPayload(), map((res: ConfigurationProperty) => res?.values[0].toLowerCase() === 'true') ).subscribe((res: boolean) => { this.registrationVerification = res; - }); + })); - this.disableUntilCheckedFcn().subscribe((res) => { + this.subscriptions.push(this.disableUntilCheckedFcn().subscribe((res) => { this.disableUntilChecked = res; this.changeDetectorRef.detectChanges(); - }); - + })); } /** @@ -112,7 +150,7 @@ export class RegisterEmailFormComponent implements OnInit { register(tokenV2?) { if (!this.form.invalid) { if (this.registrationVerification) { - combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( + this.subscriptions.push(combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( switchMap(([captchaVersion, captchaMode]) => { if (captchaVersion === 'v3') { return this.googleRecaptchaService.getRecaptchaToken('register_email'); @@ -134,7 +172,7 @@ export class RegisterEmailFormComponent implements OnInit { this.showNotification('error'); } } - ); + )); } else { this.registration(); } @@ -146,18 +184,20 @@ export class RegisterEmailFormComponent implements OnInit { */ registration(captchaToken = null) { let registerEmail$ = captchaToken ? - this.epersonRegistrationService.registerEmail(this.email.value, captchaToken) : - this.epersonRegistrationService.registerEmail(this.email.value); - registerEmail$.subscribe((response: RemoteData) => { + this.epersonRegistrationService.registerEmail(this.email.value, captchaToken, this.typeRequest) : + this.epersonRegistrationService.registerEmail(this.email.value, null, this.typeRequest); + this.subscriptions.push(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})); this.router.navigate(['/home']); + } else if (response.statusCode === 422) { + this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.error.maildomain`, {domains: this.validMailDomains.join(', ')})); } else { this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value})); } - }); + })); } /** diff --git a/src/app/register-page/register-email/register-email.component.html b/src/app/register-page/register-email/register-email.component.html index a60dc4c31e..1829bb2914 100644 --- a/src/app/register-page/register-email/register-email.component.html +++ b/src/app/register-page/register-email/register-email.component.html @@ -1,3 +1,3 @@ + [MESSAGE_PREFIX]="'register-page.registration'" [typeRequest]="typeRequest"> diff --git a/src/app/register-page/register-email/register-email.component.ts b/src/app/register-page/register-email/register-email.component.ts index 7b7b0f631b..228e8c56a0 100644 --- a/src/app/register-page/register-email/register-email.component.ts +++ b/src/app/register-page/register-email/register-email.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { TYPE_REQUEST_REGISTER } from '../../register-email-form/register-email-form.component'; @Component({ selector: 'ds-register-email', @@ -9,5 +10,5 @@ import { Component } from '@angular/core'; * Component responsible the email registration step when registering as a new user */ export class RegisterEmailComponent { - + typeRequest = TYPE_REQUEST_REGISTER; } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index f68c0ff2ce..99949c4378 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1538,7 +1538,7 @@ "forgot-email.form.email.error.required": "Please fill in an email address", - "forgot-email.form.email.error.pattern": "Please fill in a valid email address", + "forgot-email.form.email.error.not-email-form": "Please fill in a valid email address", "forgot-email.form.email.hint": "An email will be sent to this address with a further instructions.", @@ -3342,7 +3342,9 @@ "register-page.registration.email.error.required": "Please fill in an email address", - "register-page.registration.email.error.pattern": "Please fill in a valid email address", + "register-page.registration.email.error.not-email-form": "Please fill in a valid email address.", + + "register-page.registration.email.error.not-valid-domain": "Use email with allowed domains: {{ domains }}", "register-page.registration.email.hint": "This address will be verified and used as your login name.", @@ -3359,6 +3361,8 @@ "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.error.maildomain": "This email address is not on the list of domains who can register. Allowed domains are {{ domains }}", + "register-page.registration.google-recaptcha.open-cookie-settings": "Open cookie settings", @@ -3367,6 +3371,7 @@ "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.", + "register-page.registration.info.maildomain": "Accounts can be registered for mail addresses of the domains", "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items",