From 6f9f4ec9683291f3aa34392a88c2fa15986cfc56 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 15 Jun 2020 11:59:25 +0200 Subject: [PATCH 1/2] 71304: Implement Forgot password components --- src/app/app-routing.module.ts | 8 + .../data/eperson-registration.service.spec.ts | 11 +- .../core/data/eperson-registration.service.ts | 2 +- .../core/eperson/eperson-data.service.spec.ts | 10 ++ src/app/core/eperson/eperson-data.service.ts | 30 ++++ .../forgot-email.component.html | 3 + .../forgot-email.component.spec.ts | 29 ++++ .../forgot-email.component.ts | 12 ++ .../forgot-password-form.component.html | 36 +++++ .../forgot-password-form.component.spec.ts | 117 +++++++++++++++ .../forgot-password-form.component.ts | 87 +++++++++++ .../forgot-password-routing.module.ts | 32 ++++ .../forgot-password/forgot-password.module.ts | 31 ++++ .../profile-page-security-form.component.html | 9 +- ...ofile-page-security-form.component.spec.ts | 81 +++------- .../profile-page-security-form.component.ts | 116 +++++++-------- .../profile-page/profile-page.component.html | 6 +- .../profile-page.component.spec.ts | 77 ++++++++-- .../profile-page/profile-page.component.ts | 95 ++++++++++-- src/app/profile-page/profile-page.module.ts | 3 + .../register-email-form.component.html | 36 +++++ .../register-email-form.component.spec.ts | 92 ++++++++++++ .../register-email-form.component.ts | 73 +++++++++ .../register-email-form.module.ts | 26 ++++ .../registration.resolver.spec.ts | 3 +- .../registration.resolver.ts | 0 .../create-profile.component.html | 140 ++++++++---------- .../create-profile.component.spec.ts | 43 +++++- .../create-profile.component.ts | 68 ++++----- .../register-email.component.html | 39 +---- .../register-email.component.spec.ts | 70 +-------- .../register-email.component.ts | 58 +------- .../register-page-routing.module.ts | 4 +- src/app/register-page/register-page.module.ts | 7 + src/app/shared/log-in/log-in.component.html | 2 +- src/app/shared/log-in/log-in.component.ts | 6 +- src/assets/i18n/en.json5 | 68 ++++++++- 37 files changed, 1087 insertions(+), 443 deletions(-) create mode 100644 src/app/forgot-password/forgot-password-email/forgot-email.component.html create mode 100644 src/app/forgot-password/forgot-password-email/forgot-email.component.spec.ts create mode 100644 src/app/forgot-password/forgot-password-email/forgot-email.component.ts create mode 100644 src/app/forgot-password/forgot-password-form/forgot-password-form.component.html create mode 100644 src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts create mode 100644 src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts create mode 100644 src/app/forgot-password/forgot-password-routing.module.ts create mode 100644 src/app/forgot-password/forgot-password.module.ts create mode 100644 src/app/register-email-form/register-email-form.component.html create mode 100644 src/app/register-email-form/register-email-form.component.spec.ts create mode 100644 src/app/register-email-form/register-email-form.component.ts create mode 100644 src/app/register-email-form/register-email-form.module.ts rename src/app/{register-page => register-email-form}/registration.resolver.spec.ts (91%) rename src/app/{register-page => register-email-form}/registration.resolver.ts (100%) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index db0a09662b..aace169f6c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -52,6 +52,13 @@ export function getRegisterPath() { } +const FORGOT_PASSWORD_PATH = 'forgot'; + +export function getForgotPasswordPath() { + return `/${FORGOT_PASSWORD_PATH}`; + +} + const WORKFLOW_ITEM_MODULE_PATH = 'workflowitems'; export function getWorkflowItemModulePath() { @@ -79,6 +86,7 @@ export function getDSOPath(dso: DSpaceObject): string { { path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, { path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, { path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule' }, + { path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule' }, { path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' }, { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index d8623e7046..4c91ffd4f1 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -16,6 +16,10 @@ describe('EpersonRegistrationService', () => { const registration = new Registration(); registration.email = 'test@mail.org'; + const registrationWithUser = new Registration(); + registrationWithUser.email = 'test@mail.org'; + registrationWithUser.user = 'test-uuid'; + beforeEach(() => { halService = new HALEndpointServiceStub('rest-url'); @@ -65,7 +69,7 @@ describe('EpersonRegistrationService', () => { beforeEach(() => { (requestService.getByUUID as jasmine.Spy).and.returnValue( cold('a', - {a: Object.assign(new RequestEntry(), {response: new RegistrationSuccessResponse(registration, 200, 'Success')})}) + {a: Object.assign(new RequestEntry(), {response: new RegistrationSuccessResponse(registrationWithUser, 200, 'Success')})}) ); }); it('should return a registration corresponding to the provided token', () => { @@ -73,8 +77,9 @@ describe('EpersonRegistrationService', () => { expect(expected).toBeObservable(cold('(a|)', { a: Object.assign(new Registration(), { - email: registration.email, - token: 'test-token' + email: registrationWithUser.email, + token: 'test-token', + user: registrationWithUser.user }) })); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index a1ca1dbef4..df1e8580e8 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -98,7 +98,7 @@ export class EpersonRegistrationService { return this.requestService.getByUUID(requestId).pipe( filterSuccessfulResponses(), map((restResponse: RegistrationSuccessResponse) => { - return Object.assign(new Registration(), {email: restResponse.registration.email, token: token}); + return Object.assign(new Registration(), {email: restResponse.registration.email, token: token, user: restResponse.registration.user}); }), take(1), ); diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index 1dd4e8dbd3..a1a6951545 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -299,6 +299,16 @@ describe('EPersonDataService', () => { expect(requestService.configure).toHaveBeenCalledWith(expected); }); }); + describe('patchPasswordWithToken', () => { + it('should sent a patch request with an uuid, token and new password to the epersons endpoint', () => { + service.patchPasswordWithToken('test-uuid', 'test-token','test-password'); + + const operation = Object.assign({ op: 'replace', path: '/password', value: 'test-password' }); + const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); }); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index 672793bc20..8723e2dc87 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -28,6 +28,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; import { EPerson } from './models/eperson.model'; import { EPERSON } from './models/eperson.resource-type'; +import { RequestEntry } from '../data/request.reducer'; const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry; const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson); @@ -270,4 +271,33 @@ export class EPersonDataService extends DataService { } + /** + * Sends a patch request to update an epersons password based on a forgot password token + * @param uuid Uuid of the eperson + * @param token The forgot password token + * @param password The new password value + */ + patchPasswordWithToken(uuid: string, token: string, password: string): Observable { + const requestId = this.requestService.generateRequestId(); + + const operation = Object.assign({ op: 'replace', path: '/password', value: password }); + + const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, uuid)), + map((href: string) => `${href}?token=${token}`)); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PatchRequest(requestId, href, [operation]); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response) + ); + } + } 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 new file mode 100644 index 0000000000..263f142c2e --- /dev/null +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.html @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/src/app/forgot-password/forgot-password-email/forgot-email.component.spec.ts b/src/app/forgot-password/forgot-password-email/forgot-email.component.spec.ts new file mode 100644 index 0000000000..d466cbf11b --- /dev/null +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.spec.ts @@ -0,0 +1,29 @@ +import { ForgotEmailComponent } from './forgot-email.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ReactiveFormsModule } from '@angular/forms'; + +describe('ForgotEmailComponent', () => { + let comp: ForgotEmailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, TranslateModule.forRoot(), ReactiveFormsModule], + declarations: [ForgotEmailComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(ForgotEmailComponent); + comp = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should be defined', () => { + expect(comp).toBeDefined(); + }); +}); 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 new file mode 100644 index 0000000000..5e18aff113 --- /dev/null +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-forgot-email', + templateUrl: './forgot-email.component.html' +}) +/** + * Component responsible the forgot password email step + */ +export class ForgotEmailComponent { + +} diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.html b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.html new file mode 100644 index 0000000000..06a1909f00 --- /dev/null +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.html @@ -0,0 +1,36 @@ +
+

{{'forgot-password.form.head' | translate}}

+
+
{{'forgot-password.form.identification.header' | translate}}
+
+
+
+ + {{(registration$ |async).email}}
+
+
+
+ +
+
{{'forgot-password.form.card.security' | translate}}
+
+ + +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts new file mode 100644 index 0000000000..7c23a2af94 --- /dev/null +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts @@ -0,0 +1,117 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { RestResponse } from '../../core/cache/response.models'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { CoreState } from '../../core/core.reducers'; +import { Registration } from '../../core/shared/registration.model'; +import { ForgotPasswordFormComponent } from './forgot-password-form.component'; +import { By } from '@angular/platform-browser'; +import { AuthenticateAction } from '../../core/auth/auth.actions'; + +describe('ForgotPasswordFormComponent', () => { + let comp: ForgotPasswordFormComponent; + let fixture: ComponentFixture; + + let router; + let route; + let ePersonDataService: EPersonDataService; + let notificationsService; + let store: Store; + + const registration = Object.assign(new Registration(), { + email: 'test@email.org', + user: 'test-uuid', + token: 'test-token' + }); + + beforeEach(async(() => { + + route = {data: observableOf({registration: registration})}; + router = new RouterStub(); + notificationsService = new NotificationsServiceStub(); + + ePersonDataService = jasmine.createSpyObj('ePersonDataService', { + patchPasswordWithToken: observableOf(new RestResponse(true, 200, 'Success')) + }); + + store = jasmine.createSpyObj('store', { + dispatch: {}, + }); + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule], + declarations: [ForgotPasswordFormComponent], + providers: [ + {provide: Router, useValue: router}, + {provide: ActivatedRoute, useValue: route}, + {provide: Store, useValue: store}, + {provide: EPersonDataService, useValue: ePersonDataService}, + {provide: FormBuilder, useValue: new FormBuilder()}, + {provide: NotificationsService, useValue: notificationsService}, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(ForgotPasswordFormComponent); + comp = fixture.componentInstance; + + fixture.detectChanges(); + }); + + describe('init', () => { + it('should initialise mail address', () => { + const elem = fixture.debugElement.queryAll(By.css('span#email'))[0].nativeElement; + expect(elem.innerHTML).toContain('test@email.org'); + }); + }); + + describe('submit', () => { + + it('should submit a patch request for the user uuid and log in on success', () => { + comp.password = 'password'; + comp.isInValid = false; + + comp.submit(); + + expect(ePersonDataService.patchPasswordWithToken).toHaveBeenCalledWith('test-uuid', 'test-token', 'password'); + expect(store.dispatch).toHaveBeenCalledWith(new AuthenticateAction('test@email.org', 'password')); + expect(router.navigate).toHaveBeenCalledWith(['/home']); + expect(notificationsService.success).toHaveBeenCalled(); + }); + + it('should submit a patch request for the user uuid and stay on page on error', () => { + + (ePersonDataService.patchPasswordWithToken as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 500, 'Error'))); + + comp.password = 'password'; + comp.isInValid = false; + + comp.submit(); + + expect(ePersonDataService.patchPasswordWithToken).toHaveBeenCalledWith('test-uuid', 'test-token', 'password'); + expect(store.dispatch).not.toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + }); + it('should submit a patch request for the user uuid when the form is invalid', () => { + + comp.password = 'password'; + comp.isInValid = true; + + comp.submit(); + + expect(ePersonDataService.patchPasswordWithToken).not.toHaveBeenCalled(); + }); + }) +}); diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts new file mode 100644 index 0000000000..1ad66a0b04 --- /dev/null +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts @@ -0,0 +1,87 @@ +import { Component } from '@angular/core'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { ErrorResponse, RestResponse } from '../../core/cache/response.models'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { Observable } from 'rxjs'; +import { Registration } from '../../core/shared/registration.model'; +import { map } from 'rxjs/operators'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthenticateAction } from '../../core/auth/auth.actions'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../../core/core.reducers'; + +@Component({ + selector: 'ds-forgot-password-form', + templateUrl: './forgot-password-form.component.html' +}) +/** + * Component for a user to enter a new password for a forgot token. + */ +export class ForgotPasswordFormComponent { + + registration$: Observable; + + token: string; + email: string; + user: string; + + isInValid = true; + password: string; + + /** + * Prefix for the notification messages of this component + */ + NOTIFICATIONS_PREFIX = 'forgot-password.form.notification'; + + constructor(private ePersonDataService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private store: Store, + private router: Router, + private route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.registration$ = this.route.data.pipe( + map((data) => data.registration as Registration), + ); + this.registration$.subscribe((registration: Registration) => { + this.email = registration.email; + this.token = registration.token; + this.user = registration.user; + }); + } + + setInValid($event: boolean) { + this.isInValid = $event; + } + + setPasswordValue($event: string) { + this.password = $event; + } + + /** + * Submits the password to the eperson service to be updated. + * The submission will not be made when the form is not valid. + */ + submit() { + if (!this.isInValid) { + this.ePersonDataService.patchPasswordWithToken(this.user, this.token, this.password).subscribe((response: RestResponse) => { + if (response.isSuccessful) { + this.notificationsService.success( + this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.success.title'), + this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.success.content') + ); + this.store.dispatch(new AuthenticateAction(this.email, this.password)); + this.router.navigate(['/home']); + } else { + this.notificationsService.error( + this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.error.title'), (response as ErrorResponse).errorMessage + ); + } + }); + } + } +} diff --git a/src/app/forgot-password/forgot-password-routing.module.ts b/src/app/forgot-password/forgot-password-routing.module.ts new file mode 100644 index 0000000000..702de03a9d --- /dev/null +++ b/src/app/forgot-password/forgot-password-routing.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ItemPageResolver } from '../+item-page/item-page.resolver'; +import { RegistrationResolver } from '../register-email-form/registration.resolver'; +import { ForgotPasswordFormComponent } from './forgot-password-form/forgot-password-form.component'; +import { ForgotEmailComponent } from './forgot-password-email/forgot-email.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: ForgotEmailComponent, + data: {title: 'forgot-password.title'}, + }, + { + path: ':token', + component: ForgotPasswordFormComponent, + resolve: {registration: RegistrationResolver} + } + ]) + ], + providers: [ + RegistrationResolver, + ItemPageResolver + ] +}) +/** + * This module defines the routing to the components related to the forgot password components. + */ +export class ForgotPasswordRoutingModule { +} diff --git a/src/app/forgot-password/forgot-password.module.ts b/src/app/forgot-password/forgot-password.module.ts new file mode 100644 index 0000000000..5f01f0fcd2 --- /dev/null +++ b/src/app/forgot-password/forgot-password.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { ForgotEmailComponent } from './forgot-password-email/forgot-email.component'; +import { ForgotPasswordRoutingModule } from './forgot-password-routing.module'; +import { RegisterEmailFormModule } from '../register-email-form/register-email-form.module'; +import { ForgotPasswordFormComponent } from './forgot-password-form/forgot-password-form.component'; +import { ProfilePageModule } from '../profile-page/profile-page.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ForgotPasswordRoutingModule, + RegisterEmailFormModule, + ProfilePageModule, + ], + declarations: [ + ForgotEmailComponent, + ForgotPasswordFormComponent + ], + providers: [], + entryComponents: [] +}) + +/** + * Module related to the Forgot Password components + */ +export class ForgotPasswordModule { + +} diff --git a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.html b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.html index 50a081c6f2..ad9f768297 100644 --- a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.html +++ b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.html @@ -1,9 +1,10 @@ -
{{'profile.security.form.info' | translate}}
+
{{FORM_PREFIX + 'info' | translate}}
-
{{'profile.security.form.error.password-length' | translate}}
-
{{'profile.security.form.error.matching-passwords' | translate}}
+
{{FORM_PREFIX + 'error.password-length' | translate}}
+
{{FORM_PREFIX + 'error.matching-passwords' | translate}}
+
{{FORM_PREFIX + 'error.empty-password' | translate}}
diff --git a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.spec.ts b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.spec.ts index 225bd8507e..ba487f7158 100644 --- a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.spec.ts +++ b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.spec.ts @@ -1,5 +1,4 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { EPerson } from '../../core/eperson/models/eperson.model'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { VarDirective } from '../../shared/utils/var.directive'; import { TranslateModule } from '@ngx-translate/core'; @@ -15,18 +14,10 @@ describe('ProfilePageSecurityFormComponent', () => { let component: ProfilePageSecurityFormComponent; let fixture: ComponentFixture; - let user; - let epersonService; let notificationsService; function init() { - user = Object.assign(new EPerson(), { - _links: { - self: { href: 'user-selflink' } - } - }); - epersonService = jasmine.createSpyObj('epersonService', { patch: observableOf(new RestResponse(true, 200, 'OK')) }); @@ -43,8 +34,8 @@ describe('ProfilePageSecurityFormComponent', () => { declarations: [ProfilePageSecurityFormComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ - { provide: EPersonDataService, useValue: epersonService }, - { provide: NotificationsService, useValue: notificationsService }, + {provide: EPersonDataService, useValue: epersonService}, + {provide: NotificationsService, useValue: notificationsService}, FormBuilderService ], schemas: [NO_ERRORS_SCHEMA] @@ -54,65 +45,35 @@ describe('ProfilePageSecurityFormComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ProfilePageSecurityFormComponent); component = fixture.componentInstance; - component.user = user; fixture.detectChanges(); }); - describe('updateSecurity', () => { - describe('when no values changed', () => { - let result; - + describe('On value change', () => { + describe('when the password has changed', () => { beforeEach(() => { - result = component.updateSecurity(); + component.formGroup.patchValue({password: 'password'}); + component.formGroup.patchValue({passwordrepeat: 'password'}); }); - it('should return false', () => { - expect(result).toEqual(false); - }); + it('should emit the value and validity on password change with invalid validity', fakeAsync(() => { + spyOn(component.passwordValue, 'emit'); + spyOn(component.isInvalid, 'emit'); + component.formGroup.patchValue({password: 'new-password'}); - it('should not call epersonService.patch', () => { - expect(epersonService.patch).not.toHaveBeenCalled(); - }); - }); + tick(300); - describe('when password is filled in, but the confirm field is empty', () => { - let result; + expect(component.passwordValue.emit).toHaveBeenCalledWith('new-password'); + expect(component.isInvalid.emit).toHaveBeenCalledWith(true); + })); - beforeEach(() => { - setModelValue('password', 'test'); - result = component.updateSecurity(); - }); + it('should emit the value on password change', fakeAsync(() => { + spyOn(component.passwordValue, 'emit'); + component.formGroup.patchValue({password: 'new-password'}); - it('should return true', () => { - expect(result).toEqual(true); - }); - }); + tick(300); - describe('when both password fields are filled in, long enough and equal', () => { - let result; - let operations; - - beforeEach(() => { - setModelValue('password', 'testest'); - setModelValue('passwordrepeat', 'testest'); - operations = [{ op: 'replace', path: '/password', value: 'testest' }]; - result = component.updateSecurity(); - }); - - it('should return true', () => { - expect(result).toEqual(true); - }); - - it('should return call epersonService.patch', () => { - expect(epersonService.patch).toHaveBeenCalledWith(user, operations); - }); + expect(component.passwordValue.emit).toHaveBeenCalledWith('new-password'); + })); }); }); - - function setModelValue(id: string, value: string) { - component.formGroup.patchValue({ - [id]: value - }); - component.formGroup.markAllAsTouched(); - } }); diff --git a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.ts b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.ts index b8ac07e6d8..1013cad44b 100644 --- a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.ts +++ b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.ts @@ -1,16 +1,12 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { - DynamicFormControlModel, - DynamicFormService, - DynamicInputModel -} from '@ng-dynamic-forms/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { DynamicFormControlModel, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; import { FormGroup } from '@angular/forms'; -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty } from '../../shared/empty.util'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; -import { EPerson } from '../../core/eperson/models/eperson.model'; -import { ErrorResponse, RestResponse } from '../../core/cache/response.models'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { debounceTime, map } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; @Component({ selector: 'ds-profile-page-security-form', @@ -21,10 +17,15 @@ import { NotificationsService } from '../../shared/notifications/notifications.s * Displays a form containing a password field and a confirmation of the password */ export class ProfilePageSecurityFormComponent implements OnInit { + /** - * The user to display the form for + * Emits the validity of the password */ - @Input() user: EPerson; + @Output() isInvalid = new EventEmitter(); + /** + * Emits the value of the password + */ + @Output() passwordValue = new EventEmitter(); /** * The form's input models @@ -48,14 +49,17 @@ export class ProfilePageSecurityFormComponent implements OnInit { formGroup: FormGroup; /** - * Prefix for the notification messages of this component + * Indicates whether the "checkPasswordEmpty" needs to be added or not */ - NOTIFICATIONS_PREFIX = 'profile.security.form.notifications.'; + @Input() + passwordCanBeEmpty = true; /** * Prefix for the form's label messages of this component */ - LABEL_PREFIX = 'profile.security.form.label.'; + @Input() + FORM_PREFIX: string; + private subs: Subscription[] = []; constructor(protected formService: DynamicFormService, protected translate: TranslateService, @@ -64,12 +68,35 @@ export class ProfilePageSecurityFormComponent implements OnInit { } ngOnInit(): void { - this.formGroup = this.formService.createFormGroup(this.formModel, { validators: [this.checkPasswordsEqual, this.checkPasswordLength] }); + if (this.passwordCanBeEmpty) { + this.formGroup = this.formService.createFormGroup(this.formModel, + {validators: [this.checkPasswordsEqual, this.checkPasswordLength]}); + } else { + this.formGroup = this.formService.createFormGroup(this.formModel, + {validators: [this.checkPasswordsEqual, this.checkPasswordLength, this.checkPasswordEmpty]}); + } this.updateFieldTranslations(); this.translate.onLangChange .subscribe(() => { this.updateFieldTranslations(); }); + + this.subs.push(this.formGroup.statusChanges.pipe( + debounceTime(300), + map((status: string) => { + if (status !== 'VALID') { + return true; + } else { + return false; + } + })).subscribe((status) => this.isInvalid.emit(status)) + ); + + this.subs.push(this.formGroup.valueChanges.pipe( + debounceTime(300), + ).subscribe((valueChange) => { + this.passwordValue.emit(valueChange.password); + })); } /** @@ -78,7 +105,7 @@ export class ProfilePageSecurityFormComponent implements OnInit { updateFieldTranslations() { this.formModel.forEach( (fieldModel: DynamicInputModel) => { - fieldModel.label = this.translate.instant(this.LABEL_PREFIX + fieldModel.id); + fieldModel.label = this.translate.instant(this.FORM_PREFIX + 'label.' + fieldModel.id); } ); } @@ -91,7 +118,7 @@ export class ProfilePageSecurityFormComponent implements OnInit { const pass = group.get('password').value; const repeatPass = group.get('passwordrepeat').value; - return pass === repeatPass ? null : { notSame: true }; + return pass === repeatPass ? null : {notSame: true}; } /** @@ -101,51 +128,24 @@ export class ProfilePageSecurityFormComponent implements OnInit { checkPasswordLength(group: FormGroup) { const pass = group.get('password').value; - return isEmpty(pass) || pass.length >= 6 ? null : { notLongEnough: true }; + return isEmpty(pass) || pass.length >= 6 ? null : {notLongEnough: true}; } /** - * Update the user's security details - * - * Sends a patch request for changing the user's password when a new password is present and the password confirmation - * matches the new password. - * Nothing happens when no passwords are filled in. - * An error notification is displayed when the password confirmation does not match the new password. - * - * Returns false when nothing happened + * Checks if the password is empty + * @param group The FormGroup to validate */ - updateSecurity() { - const pass = this.formGroup.get('password').value; - const passEntered = isNotEmpty(pass); - if (!this.formGroup.valid) { - if (passEntered) { - if (this.checkPasswordsEqual(this.formGroup) != null) { - this.notificationsService.error(this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.not-same')); - } - if (this.checkPasswordLength(this.formGroup) != null) { - this.notificationsService.error(this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.not-long-enough')); - } - return true; - } - return false; - } - if (passEntered) { - const operation = Object.assign({ op: 'replace', path: '/password', value: pass }); - this.epersonService.patch(this.user, [operation]).subscribe((response: RestResponse) => { - if (response.isSuccessful) { - this.notificationsService.success( - this.translate.instant(this.NOTIFICATIONS_PREFIX + 'success.title'), - this.translate.instant(this.NOTIFICATIONS_PREFIX + 'success.content') - ); - } else { - this.notificationsService.error( - this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.title'), (response as ErrorResponse).errorMessage - ); - } - }); + checkPasswordEmpty(group: FormGroup) { + const pass = group.get('password').value; + return isEmpty(pass) ? {emptyPassword: true} : null; + } - } - - return passEntered; + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); } } diff --git a/src/app/profile-page/profile-page.component.html b/src/app/profile-page/profile-page.component.html index b6e62665b4..ab8f1ce026 100644 --- a/src/app/profile-page/profile-page.component.html +++ b/src/app/profile-page/profile-page.component.html @@ -10,7 +10,11 @@
{{'profile.card.security' | translate}}
- +
diff --git a/src/app/profile-page/profile-page.component.spec.ts b/src/app/profile-page/profile-page.component.spec.ts index d63aba46f5..8d78539bab 100644 --- a/src/app/profile-page/profile-page.component.spec.ts +++ b/src/app/profile-page/profile-page.component.spec.ts @@ -13,8 +13,9 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { authReducer } from '../core/auth/auth.reducer'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createPaginatedList } from '../shared/testing/utils.test'; -import { of } from 'rxjs/internal/observable/of'; +import { of as observableOf } from 'rxjs'; import { AuthService } from '../core/auth/auth.service'; +import { RestResponse } from '../core/cache/response.models'; describe('ProfilePageComponent', () => { let component: ProfilePageComponent; @@ -40,10 +41,11 @@ describe('ProfilePageComponent', () => { }; authService = jasmine.createSpyObj('authService', { - getAuthenticatedUserFromStore: of(user) + getAuthenticatedUserFromStore: observableOf(user) }); epersonService = jasmine.createSpyObj('epersonService', { - findById: createSuccessfulRemoteDataObject$(user) + findById: createSuccessfulRemoteDataObject$(user), + patch: observableOf(Object.assign(new RestResponse(true, 200, 'Success'))) }); notificationsService = jasmine.createSpyObj('notificationsService', { success: {}, @@ -84,9 +86,7 @@ describe('ProfilePageComponent', () => { component.metadataForm = jasmine.createSpyObj('metadataForm', { updateProfile: false }); - component.securityForm = jasmine.createSpyObj('securityForm', { - updateSecurity: true - }); + spyOn(component, 'updateSecurity').and.returnValue(true); component.updateProfile(); }); @@ -100,9 +100,6 @@ describe('ProfilePageComponent', () => { component.metadataForm = jasmine.createSpyObj('metadataForm', { updateProfile: true }); - component.securityForm = jasmine.createSpyObj('securityForm', { - updateSecurity: false - }); component.updateProfile(); }); @@ -116,9 +113,6 @@ describe('ProfilePageComponent', () => { component.metadataForm = jasmine.createSpyObj('metadataForm', { updateProfile: true }); - component.securityForm = jasmine.createSpyObj('securityForm', { - updateSecurity: true - }); component.updateProfile(); }); @@ -132,9 +126,6 @@ describe('ProfilePageComponent', () => { component.metadataForm = jasmine.createSpyObj('metadataForm', { updateProfile: false }); - component.securityForm = jasmine.createSpyObj('securityForm', { - updateSecurity: false - }); component.updateProfile(); }); @@ -143,4 +134,60 @@ describe('ProfilePageComponent', () => { }); }); }); + + describe('updateSecurity', () => { + describe('when no password value present', () => { + let result; + + beforeEach(() => { + component.setPasswordValue(''); + + result = component.updateSecurity(); + }); + + it('should return false', () => { + expect(result).toEqual(false); + }); + + it('should not call epersonService.patch', () => { + expect(epersonService.patch).not.toHaveBeenCalled(); + }); + }); + + describe('when password is filled in, but the password is invalid', () => { + let result; + + beforeEach(() => { + component.setPasswordValue('test'); + component.setInvalid(true); + result = component.updateSecurity(); + }); + + it('should return true', () => { + expect(result).toEqual(true); + expect(epersonService.patch).not.toHaveBeenCalled(); + }); + }); + + describe('when password is filled in, and is valid', () => { + let result; + let operations; + + beforeEach(() => { + component.setPasswordValue('testest'); + component.setInvalid(false); + + operations = [{op: 'replace', path: '/password', value: 'testest'}]; + result = component.updateSecurity(); + }); + + it('should return true', () => { + expect(result).toEqual(true); + }); + + it('should return call epersonService.patch', () => { + expect(epersonService.patch).toHaveBeenCalledWith(user, operations); + }); + }); + }); }); diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts index 8f4cd492a2..bc06c49f81 100644 --- a/src/app/profile-page/profile-page.component.ts +++ b/src/app/profile-page/profile-page.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; import { EPerson } from '../core/eperson/models/eperson.model'; import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component'; -import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { Group } from '../core/eperson/models/group.model'; @@ -11,9 +10,10 @@ import { PaginatedList } from '../core/data/paginated-list'; import { filter, switchMap, tap } from 'rxjs/operators'; import { EPersonDataService } from '../core/eperson/eperson-data.service'; import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators'; -import { hasValue } from '../shared/empty.util'; +import { hasValue, isNotEmpty } from '../shared/empty.util'; import { followLink } from '../shared/utils/follow-link-config.model'; import { AuthService } from '../core/auth/auth.service'; +import { ErrorResponse, RestResponse } from '../core/cache/response.models'; @Component({ selector: 'ds-profile-page', @@ -26,15 +26,10 @@ export class ProfilePageComponent implements OnInit { /** * A reference to the metadata form component */ - @ViewChild(ProfilePageMetadataFormComponent, { static: false }) metadataForm: ProfilePageMetadataFormComponent; + @ViewChild(ProfilePageMetadataFormComponent, {static: false}) metadataForm: ProfilePageMetadataFormComponent; /** - * A reference to the security form component - */ - @ViewChild(ProfilePageSecurityFormComponent, { static: false }) securityForm: ProfilePageSecurityFormComponent; - - /** - * The authenticated user + * The authenticated user as observable */ user$: Observable; @@ -48,6 +43,26 @@ export class ProfilePageComponent implements OnInit { */ NOTIFICATIONS_PREFIX = 'profile.notifications.'; + /** + * Prefix for the notification messages of this security form + */ + PASSWORD_NOTIFICATIONS_PREFIX = 'profile.security.form.notifications.'; + + /** + * The validity of the password filled in, in the security form + */ + private invalidSecurity: boolean; + + /** + * The password filled in, in the security form + */ + private password: string; + + /** + * The authenticated user + */ + private currentUser: EPerson; + constructor(private authService: AuthService, private notificationsService: NotificationsService, private translate: TranslateService, @@ -59,7 +74,8 @@ export class ProfilePageComponent implements OnInit { filter((user: EPerson) => hasValue(user.id)), switchMap((user: EPerson) => this.epersonService.findById(user.id, followLink('groups'))), getAllSucceededRemoteData(), - getRemoteDataPayload() + getRemoteDataPayload(), + tap((user: EPerson) => this.currentUser = user) ); this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups)); } @@ -70,7 +86,7 @@ export class ProfilePageComponent implements OnInit { */ updateProfile() { const metadataChanged = this.metadataForm.updateProfile(); - const securityChanged = this.securityForm.updateSecurity(); + const securityChanged = this.updateSecurity(); if (!metadataChanged && !securityChanged) { this.notificationsService.warning( this.translate.instant(this.NOTIFICATIONS_PREFIX + 'warning.no-changes.title'), @@ -78,4 +94,61 @@ export class ProfilePageComponent implements OnInit { ); } } + + /** + * Sets the validity of the password based on an emitted of the form + * @param $event + */ + setInvalid($event: boolean) { + this.invalidSecurity = $event; + } + + /** + * Update the user's security details + * + * Sends a patch request for changing the user's password when a new password is present and the password confirmation + * matches the new password. + * Nothing happens when no passwords are filled in. + * An error notification is displayed when the password confirmation does not match the new password. + * + * Returns false when the password was empty + */ + updateSecurity() { + const passEntered = isNotEmpty(this.password); + + if (this.invalidSecurity) { + this.notificationsService.error(this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.general')); + } + if (!this.invalidSecurity && passEntered) { + const operation = Object.assign({op: 'replace', path: '/password', value: this.password}); + this.epersonService.patch(this.currentUser, [operation]).subscribe((response: RestResponse) => { + if (response.isSuccessful) { + this.notificationsService.success( + this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'success.title'), + this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'success.content') + ); + } else { + this.notificationsService.error( + this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.title'), (response as ErrorResponse).errorMessage + ); + } + }); + } + return passEntered; + } + + /** + * Set the password value based on the value emitted from the security form + * @param $event + */ + setPasswordValue($event: string) { + this.password = $event; + } + + /** + * Submit of the security form that triggers the updateProfile method + */ + submit() { + this.updateProfile(); + } } diff --git a/src/app/profile-page/profile-page.module.ts b/src/app/profile-page/profile-page.module.ts index f40c125ff8..54b59c97ce 100644 --- a/src/app/profile-page/profile-page.module.ts +++ b/src/app/profile-page/profile-page.module.ts @@ -12,6 +12,9 @@ import { ProfilePageSecurityFormComponent } from './profile-page-security-form/p CommonModule, SharedModule ], + exports: [ + ProfilePageSecurityFormComponent + ], declarations: [ ProfilePageComponent, ProfilePageMetadataFormComponent, diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html new file mode 100644 index 0000000000..e47eedb6ae --- /dev/null +++ b/src/app/register-email-form/register-email-form.component.html @@ -0,0 +1,36 @@ +
+

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

+

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

+ +
+ +
+
+
+ + +
+ + {{ MESSAGE_PREFIX + '.email.error.required' | translate }} + + + {{ MESSAGE_PREFIX + '.email.error.pattern' | translate }} + +
+
+
+ {{MESSAGE_PREFIX + '.email.hint' |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 new file mode 100644 index 0000000000..af3c70dc33 --- /dev/null +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -0,0 +1,92 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { RestResponse } from '../core/cache/response.models'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; +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'; + +describe('RegisterEmailComponent', () => { + + let comp: RegisterEmailFormComponent; + let fixture: ComponentFixture; + + let router; + let epersonRegistrationService: EpersonRegistrationService; + let notificationsService; + + beforeEach(async(() => { + + router = new RouterStub(); + notificationsService = new NotificationsServiceStub(); + + epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { + registerEmail: observableOf(new RestResponse(true, 200, 'Success')) + }); + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule], + declarations: [RegisterEmailFormComponent], + providers: [ + {provide: Router, useValue: router}, + {provide: EpersonRegistrationService, useValue: epersonRegistrationService}, + {provide: FormBuilder, useValue: new FormBuilder()}, + {provide: NotificationsService, useValue: notificationsService}, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(RegisterEmailFormComponent); + comp = fixture.componentInstance; + + fixture.detectChanges(); + }); + describe('init', () => { + it('should initialise the form', () => { + const elem = fixture.debugElement.queryAll(By.css('input#email'))[0].nativeElement; + expect(elem).toBeDefined(); + }); + }); + describe('email validation', () => { + it('should be invalid when no email is present', () => { + expect(comp.form.invalid).toBeTrue(); + }); + it('should be invalid when no valid email is present', () => { + comp.form.patchValue({email: 'invalid'}); + expect(comp.form.invalid).toBeTrue(); + }); + it('should be invalid when no valid email is present', () => { + comp.form.patchValue({email: 'valid@email.org'}); + 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(notificationsService.success).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/home']); + }); + 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'); + 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 new file mode 100644 index 0000000000..59fe3791e0 --- /dev/null +++ b/src/app/register-email-form/register-email-form.component.ts @@ -0,0 +1,73 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; +import { RestResponse } from '../core/cache/response.models'; +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'; + +@Component({ + selector: 'ds-register-email-form', + templateUrl: './register-email-form.component.html' +}) +/** + * Component responsible to render an email registration form. + */ +export class RegisterEmailFormComponent implements OnInit { + + /** + * The form containing the mail address + */ + form: FormGroup; + + /** + * The message prefix + */ + @Input() + MESSAGE_PREFIX: string; + + constructor( + private epersonRegistrationService: EpersonRegistrationService, + private notificationService: NotificationsService, + private translateService: TranslateService, + private router: Router, + private formBuilder: FormBuilder + ) { + + } + + ngOnInit(): void { + this.form = this.formBuilder.group({ + email: new FormControl('', { + validators: [Validators.required, + Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$') + ], + }) + }); + + } + + /** + * Register an email address + */ + register() { + if (!this.form.invalid) { + this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RestResponse) => { + if (response.isSuccessful) { + 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/app/register-email-form/register-email-form.module.ts b/src/app/register-email-form/register-email-form.module.ts new file mode 100644 index 0000000000..a5fe0899a9 --- /dev/null +++ b/src/app/register-email-form/register-email-form.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { RegisterEmailFormComponent } from './register-email-form.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + RegisterEmailFormComponent, + ], + providers: [], + exports: [ + RegisterEmailFormComponent, + ], + entryComponents: [] +}) + +/** + * The module that contains the components related to the email registration + */ +export class RegisterEmailFormModule { + +} diff --git a/src/app/register-page/registration.resolver.spec.ts b/src/app/register-email-form/registration.resolver.spec.ts similarity index 91% rename from src/app/register-page/registration.resolver.spec.ts rename to src/app/register-email-form/registration.resolver.spec.ts index 94f7b37a4f..8feef919a7 100644 --- a/src/app/register-page/registration.resolver.spec.ts +++ b/src/app/register-email-form/registration.resolver.spec.ts @@ -9,7 +9,7 @@ describe('RegistrationResolver', () => { let epersonRegistrationService: EpersonRegistrationService; const token = 'test-token'; - const registration = Object.assign(new Registration(), {email: 'test@email.org', token: token}); + const registration = Object.assign(new Registration(), {email: 'test@email.org', token: token, user:'user-uuid'}); beforeEach(() => { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { @@ -25,6 +25,7 @@ describe('RegistrationResolver', () => { (resolved) => { expect(resolved.token).toEqual(token); expect(resolved.email).toEqual('test@email.org'); + expect(resolved.user).toEqual('user-uuid'); } ); }); diff --git a/src/app/register-page/registration.resolver.ts b/src/app/register-email-form/registration.resolver.ts similarity index 100% rename from src/app/register-page/registration.resolver.ts rename to src/app/register-email-form/registration.resolver.ts diff --git a/src/app/register-page/create-profile/create-profile.component.html b/src/app/register-page/create-profile/create-profile.component.html index dc6a3ddeef..cfd6e7ab16 100644 --- a/src/app/register-page/create-profile/create-profile.component.html +++ b/src/app/register-page/create-profile/create-profile.component.html @@ -1,99 +1,89 @@
-

{{'register-page.create-profile.header' | translate}}

-

{{'register-page.create-profile.identification.header' | translate}}

-
-
- - {{(registration$ |async).email}}
-
-
- -
+

{{'register-page.create-profile.header' | translate}}

+
+
{{'register-page.create-profile.identification.header' | translate}}
+
- -
+ for="email">{{'register-page.create-profile.identification.email' | translate}} + {{(registration$ |async).email}}
+
+ + +
+
+
+ + +
{{ 'register-page.create-profile.identification.first-name.error' | translate }} -
-
+
+
-
-
-
- - -
+
+
+
+ + +
{{ 'register-page.create-profile.identification.last-name.error' | translate }} +
+
+
+
+
+ + +
+
+
+
+ + + +
-
-
-
- - -
-
-
-
- - - -
-
+
- +
-

{{'register-page.create-profile.security.header' | translate}}

-

{{'register-page.create-profile.security.info' | translate}}

+
+
{{'register-page.create-profile.security.header' | translate}}
+
-
-
-
-
- - -
-
-
-
- - -
- {{ 'register-page.create-profile.security.password.error' | translate }} -
-
-
+
-
+
+
+ [disabled]="isInValidPassword || userInfoForm.invalid" + class="btn btn-default btn-primary" + (click)="submitEperson()">{{'register-page.create-profile.submit' | translate}}
diff --git a/src/app/register-page/create-profile/create-profile.component.spec.ts b/src/app/register-page/create-profile/create-profile.component.spec.ts index 5fed324a22..f3017ba918 100644 --- a/src/app/register-page/create-profile/create-profile.component.spec.ts +++ b/src/app/register-page/create-profile/create-profile.component.spec.ts @@ -116,15 +116,11 @@ describe('CreateProfileComponent', () => { const lastName = fixture.debugElement.queryAll(By.css('input#lastName'))[0].nativeElement; const contactPhone = fixture.debugElement.queryAll(By.css('input#contactPhone'))[0].nativeElement; const language = fixture.debugElement.queryAll(By.css('select#language'))[0].nativeElement; - const password = fixture.debugElement.queryAll(By.css('input#password'))[0].nativeElement; - const confirmPassword = fixture.debugElement.queryAll(By.css('input#confirmPassword'))[0].nativeElement; expect(firstName).toBeDefined(); expect(lastName).toBeDefined(); expect(contactPhone).toBeDefined(); expect(language).toBeDefined(); - expect(password).toBeDefined(); - expect(confirmPassword).toBeDefined(); }); }); @@ -135,8 +131,8 @@ describe('CreateProfileComponent', () => { comp.lastName.patchValue('Last'); comp.contactPhone.patchValue('Phone'); comp.language.patchValue('en'); - comp.password.patchValue('password'); - comp.confirmPassword.patchValue('password'); + comp.password = 'password'; + comp.isInValidPassword = false; comp.submitEperson(); @@ -154,8 +150,8 @@ describe('CreateProfileComponent', () => { comp.lastName.patchValue('Last'); comp.contactPhone.patchValue('Phone'); comp.language.patchValue('en'); - comp.password.patchValue('password'); - comp.confirmPassword.patchValue('password'); + comp.password = 'password'; + comp.isInValidPassword = false; comp.submitEperson(); @@ -164,5 +160,36 @@ describe('CreateProfileComponent', () => { expect(router.navigate).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); }); + it('should submit not submit an eperson when the user info form is invalid', () => { + + (ePersonDataService.createEPersonForToken as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 500, 'Error'))); + + comp.firstName.patchValue(''); + comp.lastName.patchValue('Last'); + comp.contactPhone.patchValue('Phone'); + comp.language.patchValue('en'); + comp.password = 'password'; + comp.isInValidPassword = false; + + comp.submitEperson(); + + expect(ePersonDataService.createEPersonForToken).not.toHaveBeenCalled(); + }); + it('should submit not submit an eperson when the password is invalid', () => { + + (ePersonDataService.createEPersonForToken as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 500, 'Error'))); + + comp.firstName.patchValue('First'); + comp.lastName.patchValue('Last'); + comp.contactPhone.patchValue('Phone'); + comp.language.patchValue('en'); + comp.password = 'password'; + comp.isInValidPassword = true; + + comp.submitEperson(); + + expect(ePersonDataService.createEPersonForToken).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/register-page/create-profile/create-profile.component.ts b/src/app/register-page/create-profile/create-profile.component.ts index aee9cd598d..2755a17739 100644 --- a/src/app/register-page/create-profile/create-profile.component.ts +++ b/src/app/register-page/create-profile/create-profile.component.ts @@ -1,10 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { debounceTime, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { Registration } from '../../core/shared/registration.model'; import { Observable } from 'rxjs'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; -import { ConfirmedValidator } from './confirmed.validator'; import { TranslateService } from '@ngx-translate/core'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPerson } from '../../core/eperson/models/eperson.model'; @@ -14,6 +13,7 @@ import { CoreState } from '../../core/core.reducers'; import { AuthenticateAction } from '../../core/auth/auth.actions'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { environment } from '../../../environments/environment'; +import { isEmpty } from '../../shared/empty.util'; /** * Component that renders the create profile page to be used by a user registering through a token @@ -28,11 +28,11 @@ export class CreateProfileComponent implements OnInit { email: string; token: string; - userInfoForm: FormGroup; - passwordForm: FormGroup; - activeLangs: LangConfig[]; + isInValidPassword = true; + password: string; - isValidPassWord$: Observable; + userInfoForm: FormGroup; + activeLangs: LangConfig[]; constructor( private translateService: TranslateService, @@ -67,29 +67,23 @@ export class CreateProfileComponent implements OnInit { language: new FormControl(''), }); - this.passwordForm = this.formBuilder.group({ - password: new FormControl('', { - validators: [Validators.required, Validators.minLength(6)], - updateOn: 'change' - }), - confirmPassword: new FormControl('', { - validators: [Validators.required], - updateOn: 'change' - }) - }, { - validator: ConfirmedValidator('password', 'confirmPassword') - }); + } - this.isValidPassWord$ = this.passwordForm.statusChanges.pipe( - debounceTime(300), - map((status: string) => { - if (status === 'VALID') { - return true; - } else { - return false; - } - }) - ); + /** + * Sets the validity of the password based on a value emitted from the form + * @param $event + */ + setInValid($event: boolean) { + this.isInValidPassword = $event || isEmpty(this.password); + } + + /** + * Sets the value of the password based on a value emitted from the form + * @param $event + */ + setPasswordValue($event: string) { + this.password = $event; + this.isInValidPassword = this.isInValidPassword || isEmpty(this.password); } get firstName() { @@ -108,20 +102,12 @@ export class CreateProfileComponent implements OnInit { return this.userInfoForm.get('language'); } - get password() { - return this.passwordForm.get('password'); - } - - get confirmPassword() { - return this.passwordForm.get('confirmPassword'); - } - /** * Submits the eperson to the service to be created. - * The submission will not be made when the form is not valid. + * The submission will not be made when the form or the password is not valid. */ submitEperson() { - if (!(this.userInfoForm.invalid || this.passwordForm.invalid)) { + if (!(this.userInfoForm.invalid || this.isInValidPassword)) { const values = { metadata: { 'eperson.firstname': [ @@ -146,7 +132,7 @@ export class CreateProfileComponent implements OnInit { ] }, email: this.email, - password: this.password.value, + password: this.password, canLogIn: true, requireCertificate: false }; @@ -156,14 +142,14 @@ export class CreateProfileComponent implements OnInit { if (response.isSuccessful) { this.notificationsService.success(this.translateService.get('register-page.create-profile.submit.success.head'), this.translateService.get('register-page.create-profile.submit.success.content')); - this.store.dispatch(new AuthenticateAction(this.email, this.password.value)); + this.store.dispatch(new AuthenticateAction(this.email, this.password)); this.router.navigate(['/home']); } else { this.notificationsService.error(this.translateService.get('register-page.create-profile.submit.error.head'), this.translateService.get('register-page.create-profile.submit.error.content')); } }); - } } + } 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 f506ab8f5d..a60dc4c31e 100644 --- a/src/app/register-page/register-email/register-email.component.html +++ b/src/app/register-page/register-email/register-email.component.html @@ -1,36 +1,3 @@ -
-

{{'register-page.registration.header'|translate}}

-

{{'register-page.registration.info' | translate}}

- -
- -
-
-
- - -
- - {{ 'register-page.registration.email.error.required' | translate }} - - - {{ 'register-page.registration.email.error.pattern' | translate }} - -
-
-
- {{'register-page.registration.email.hint' |translate}} -
- -
- -
-
- - -
+ + diff --git a/src/app/register-page/register-email/register-email.component.spec.ts b/src/app/register-page/register-email/register-email.component.spec.ts index 67986853ea..74bd247d74 100644 --- a/src/app/register-page/register-email/register-email.component.spec.ts +++ b/src/app/register-page/register-email/register-email.component.spec.ts @@ -1,46 +1,19 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../../core/cache/response.models'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { Router } from '@angular/router'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { ReactiveFormsModule } from '@angular/forms'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { RegisterEmailComponent } from './register-email.component'; -import { EpersonRegistrationService } from '../../core/data/eperson-registration.service'; -import { By } from '@angular/platform-browser'; -import { RouterStub } from '../../shared/testing/router.stub'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; describe('RegisterEmailComponent', () => { let comp: RegisterEmailComponent; let fixture: ComponentFixture; - let router; - let epersonRegistrationService: EpersonRegistrationService; - let notificationsService; - beforeEach(async(() => { - - router = new RouterStub(); - notificationsService = new NotificationsServiceStub(); - - epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { - registerEmail: observableOf(new RestResponse(true, 200, 'Success')) - }); - TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule], + imports: [CommonModule, TranslateModule.forRoot(), ReactiveFormsModule], declarations: [RegisterEmailComponent], - providers: [ - {provide: Router, useValue: router}, - {provide: EpersonRegistrationService, useValue: epersonRegistrationService}, - {provide: FormBuilder, useValue: new FormBuilder()}, - {provide: NotificationsService, useValue: notificationsService}, - ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); })); @@ -50,43 +23,8 @@ describe('RegisterEmailComponent', () => { fixture.detectChanges(); }); - describe('init', () => { - it('should initialise the form', () => { - const elem = fixture.debugElement.queryAll(By.css('input#email'))[0].nativeElement; - expect(elem).toBeDefined(); - }); - }); - describe('email validation', () => { - it('should be invalid when no email is present', () => { - expect(comp.form.invalid).toBeTrue(); - }); - it('should be invalid when no valid email is present', () => { - comp.form.patchValue({email: 'invalid'}); - expect(comp.form.invalid).toBeTrue(); - }); - it('should be invalid when no valid email is present', () => { - comp.form.patchValue({email: 'valid@email.org'}); - 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(notificationsService.success).toHaveBeenCalled(); - expect(router.navigate).toHaveBeenCalledWith(['/home']); - }); - 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'); - expect(notificationsService.error).toHaveBeenCalled(); - expect(router.navigate).not.toHaveBeenCalled(); - }); + it('should be defined', () => { + expect(comp).toBeDefined(); }); }); 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 c23a7797a2..ac221c109a 100644 --- a/src/app/register-page/register-email/register-email.component.ts +++ b/src/app/register-page/register-email/register-email.component.ts @@ -1,64 +1,12 @@ -import { Component, OnInit } from '@angular/core'; -import { EpersonRegistrationService } from '../../core/data/eperson-registration.service'; -import { RestResponse } from '../../core/cache/response.models'; -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 { Component } from '@angular/core'; @Component({ selector: 'ds-register-email', templateUrl: './register-email.component.html' }) /** - * Component responsible the email registration step + * Component responsible the email registration step when registering as a new user */ -export class RegisterEmailComponent implements OnInit { - - form: FormGroup; - - constructor( - private epersonRegistrationService: EpersonRegistrationService, - private notificationService: NotificationsService, - private translateService: TranslateService, - private router: Router, - private formBuilder: FormBuilder - ) { - - } - - ngOnInit(): void { - this.form = this.formBuilder.group({ - email: new FormControl('', { - validators: [Validators.required, - Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$') - ], - }) - }); - - } - - /** - * Register an email address - */ - register() { - if (!this.form.invalid) { - this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RestResponse) => { - if (response.isSuccessful) { - this.notificationService.success(this.translateService.get('register-page.registration.success.head'), - this.translateService.get('register-page.registration.success.content', {email: this.email.value})); - this.router.navigate(['/home']); - } else { - this.notificationService.error(this.translateService.get('register-page.registration.error.head'), - this.translateService.get('register-page.registration.error.content', {email: this.email.value})); - } - } - ); - } - } - - get email() { - return this.form.get('email'); - } +export class RegisterEmailComponent { } diff --git a/src/app/register-page/register-page-routing.module.ts b/src/app/register-page/register-page-routing.module.ts index ac0edd7e70..c7cceeaaf4 100644 --- a/src/app/register-page/register-page-routing.module.ts +++ b/src/app/register-page/register-page-routing.module.ts @@ -2,8 +2,8 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { RegisterEmailComponent } from './register-email/register-email.component'; import { CreateProfileComponent } from './create-profile/create-profile.component'; -import { RegistrationResolver } from './registration.resolver'; import { ItemPageResolver } from '../+item-page/item-page.resolver'; +import { RegistrationResolver } from '../register-email-form/registration.resolver'; @NgModule({ imports: [ @@ -26,7 +26,7 @@ import { ItemPageResolver } from '../+item-page/item-page.resolver'; ] }) /** - * This module defines the default component to load when navigating to the mydspace page path. + * Module related to the navigation to components used to register a new user */ export class RegisterPageRoutingModule { } diff --git a/src/app/register-page/register-page.module.ts b/src/app/register-page/register-page.module.ts index c6c45c8a23..b29d2ecaaf 100644 --- a/src/app/register-page/register-page.module.ts +++ b/src/app/register-page/register-page.module.ts @@ -4,12 +4,16 @@ import { SharedModule } from '../shared/shared.module'; import { RegisterPageRoutingModule } from './register-page-routing.module'; import { RegisterEmailComponent } from './register-email/register-email.component'; import { CreateProfileComponent } from './create-profile/create-profile.component'; +import { RegisterEmailFormModule } from '../register-email-form/register-email-form.module'; +import { ProfilePageModule } from '../profile-page/profile-page.module'; @NgModule({ imports: [ CommonModule, SharedModule, RegisterPageRoutingModule, + RegisterEmailFormModule, + ProfilePageModule, ], declarations: [ RegisterEmailComponent, @@ -19,6 +23,9 @@ import { CreateProfileComponent } from './create-profile/create-profile.componen entryComponents: [] }) +/** + * Module related to components used to register a new user + */ export class RegisterPageModule { } diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html index 2d52bf79bb..5285bc65e4 100644 --- a/src/app/shared/log-in/log-in.component.html +++ b/src/app/shared/log-in/log-in.component.html @@ -9,5 +9,5 @@ {{"login.form.new-user" | translate}} - {{"login.form.forgot-password" | translate}} + {{"login.form.forgot-password" | translate}}
diff --git a/src/app/shared/log-in/log-in.component.ts b/src/app/shared/log-in/log-in.component.ts index 6634389c26..32e10fef45 100644 --- a/src/app/shared/log-in/log-in.component.ts +++ b/src/app/shared/log-in/log-in.component.ts @@ -8,7 +8,7 @@ import { AuthMethod } from '../../core/auth/models/auth.method'; import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors'; import { CoreState } from '../../core/core.reducers'; import { AuthService } from '../../core/auth/auth.service'; -import { getRegisterPath } from '../../app-routing.module'; +import { getForgotPasswordPath, getRegisterPath } from '../../app-routing.module'; /** * /users/sign-in @@ -86,4 +86,8 @@ export class LogInComponent implements OnInit, OnDestroy { getRegisterPath() { return getRegisterPath(); } + + getForgotPath() { + return getForgotPasswordPath(); + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 12011ee18a..776ec09e1f 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -934,6 +934,62 @@ "footer.link.duraspace": "DuraSpace", + "forgot-email.form.header": "Forgot Password", + + "forgot-email.form.info": "Enter Register an account to subscribe to collections for email updates, and submit new items to DSpace.", + + "forgot-email.form.email": "Email Address *", + + "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.hint": "This address will be verified and used as your login name.", + + "forgot-email.form.submit": "Submit", + + "forgot-email.form.success.head": "Verification email sent", + + "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", + + "forgot-email.form.error.head": "Error when trying to register email", + + "forgot-email.form.error.content": "An error occured when registering the following email address: {{ email }}", + + + + "forgot-password.title": "Forgot Password", + + "forgot-password.form.head": "Forgot Password", + + "forgot-password.form.info": "Enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", + + "forgot-password.form.card.security": "Security", + + "forgot-password.form.identification.header": "Identify", + + "forgot-password.form.identification.email": "Email address: ", + + "forgot-password.form.label.password": "Password", + + "forgot-password.form.label.passwordrepeat": "Retype to confirm", + + "forgot-password.form.error.empty-password": "Please enter a password in the box below.", + + "forgot-password.form.error.matching-passwords": "The passwords do not match.", + + "forgot-password.form.error.password-length": "The password should be at least 6 characters long.", + + "forgot-password.form.notification.error.title": "Error when trying to submit new password", + + "forgot-password.form.notification.success.content": "The password reset was successful. You have been logged in as the created user.", + + "forgot-password.form.notification.success.title": "Password reset completed", + + "forgot-password.form.submit": "Submit password", + + + "form.add": "Add", "form.add-help": "Click here to add the current entry and to add another one", @@ -2066,11 +2122,15 @@ "register-page.create-profile.security.info": "Please enter a password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", - "register-page.create-profile.security.password": "Password *", + "register-page.create-profile.security.label.password": "Password *", - "register-page.create-profile.security.confirm-password": "Retype to confirm *", + "register-page.create-profile.security.label.passwordrepeat": "Retype to confirm *", - "register-page.create-profile.security.password.error": "Please enter a password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", + "register-page.create-profile.security.error.empty-password": "Please enter a password in the box below.", + + "register-page.create-profile.security.error.matching-passwords": "The passwords do not match.", + + "register-page.create-profile.security.error.password-length": "The password should be at least 6 characters long.", "register-page.create-profile.submit": "Complete Registration", @@ -2095,7 +2155,7 @@ "register-page.registration.email.hint": "This address will be verified and used as your login name.", - "register-page.registration.register": "Register", + "register-page.registration.submit": "Register", "register-page.registration.success.head": "Verification email sent", From e277a72ebf6df6aa76eb382f9eb110bab785c3bb Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 16 Jun 2020 15:58:19 +0200 Subject: [PATCH 2/2] 71304: Fix spec descriptions --- .../register-email-form/register-email-form.component.spec.ts | 2 +- .../create-profile/create-profile.component.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 af3c70dc33..8faaa4021d 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 @@ -64,7 +64,7 @@ describe('RegisterEmailComponent', () => { comp.form.patchValue({email: 'invalid'}); expect(comp.form.invalid).toBeTrue(); }); - it('should be invalid when no valid email is present', () => { + it('should be valid when a valid email is present', () => { comp.form.patchValue({email: 'valid@email.org'}); expect(comp.form.invalid).toBeFalse(); }); diff --git a/src/app/register-page/create-profile/create-profile.component.spec.ts b/src/app/register-page/create-profile/create-profile.component.spec.ts index f3017ba918..62a88d460a 100644 --- a/src/app/register-page/create-profile/create-profile.component.spec.ts +++ b/src/app/register-page/create-profile/create-profile.component.spec.ts @@ -160,7 +160,7 @@ describe('CreateProfileComponent', () => { expect(router.navigate).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); }); - it('should submit not submit an eperson when the user info form is invalid', () => { + it('should submit not create an eperson when the user info form is invalid', () => { (ePersonDataService.createEPersonForToken as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 500, 'Error'))); @@ -175,7 +175,7 @@ describe('CreateProfileComponent', () => { expect(ePersonDataService.createEPersonForToken).not.toHaveBeenCalled(); }); - it('should submit not submit an eperson when the password is invalid', () => { + it('should submit not create an eperson when the password is invalid', () => { (ePersonDataService.createEPersonForToken as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 500, 'Error')));