Merge pull request #707 from atmire/Forgot-Password

Forgot password
This commit is contained in:
Tim Donohue
2020-07-01 09:49:14 -05:00
committed by GitHub
37 changed files with 1087 additions and 443 deletions

View File

@@ -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'; const WORKFLOW_ITEM_MODULE_PATH = 'workflowitems';
export function getWorkflowItemModulePath() { 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: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
{ path: 'handle', 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: 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: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },

View File

@@ -16,6 +16,10 @@ describe('EpersonRegistrationService', () => {
const registration = new Registration(); const registration = new Registration();
registration.email = 'test@mail.org'; registration.email = 'test@mail.org';
const registrationWithUser = new Registration();
registrationWithUser.email = 'test@mail.org';
registrationWithUser.user = 'test-uuid';
beforeEach(() => { beforeEach(() => {
halService = new HALEndpointServiceStub('rest-url'); halService = new HALEndpointServiceStub('rest-url');
@@ -65,7 +69,7 @@ describe('EpersonRegistrationService', () => {
beforeEach(() => { beforeEach(() => {
(requestService.getByUUID as jasmine.Spy).and.returnValue( (requestService.getByUUID as jasmine.Spy).and.returnValue(
cold('a', 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', () => { it('should return a registration corresponding to the provided token', () => {
@@ -73,8 +77,9 @@ describe('EpersonRegistrationService', () => {
expect(expected).toBeObservable(cold('(a|)', { expect(expected).toBeObservable(cold('(a|)', {
a: Object.assign(new Registration(), { a: Object.assign(new Registration(), {
email: registration.email, email: registrationWithUser.email,
token: 'test-token' token: 'test-token',
user: registrationWithUser.user
}) })
})); }));

View File

@@ -98,7 +98,7 @@ export class EpersonRegistrationService {
return this.requestService.getByUUID(requestId).pipe( return this.requestService.getByUUID(requestId).pipe(
filterSuccessfulResponses(), filterSuccessfulResponses(),
map((restResponse: RegistrationSuccessResponse) => { 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), take(1),
); );

View File

@@ -299,6 +299,16 @@ describe('EPersonDataService', () => {
expect(requestService.configure).toHaveBeenCalledWith(expected); 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);
});
});
}); });

View File

@@ -28,6 +28,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators';
import { EPerson } from './models/eperson.model'; import { EPerson } from './models/eperson.model';
import { EPERSON } from './models/eperson.resource-type'; import { EPERSON } from './models/eperson.resource-type';
import { RequestEntry } from '../data/request.reducer';
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry; const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson); const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
@@ -270,4 +271,33 @@ export class EPersonDataService extends DataService<EPerson> {
} }
/**
* 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<RestResponse> {
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)
);
}
} }

View File

@@ -0,0 +1,3 @@
<ds-register-email-form
[MESSAGE_PREFIX]="'forgot-email.form'">
</ds-register-email-form>

View File

@@ -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<ForgotEmailComponent>;
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();
});
});

View File

@@ -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 {
}

View File

@@ -0,0 +1,36 @@
<div class="container">
<h3 class="mb-4">{{'forgot-password.form.head' | translate}}</h3>
<div class="card mb-4">
<div class="card-header">{{'forgot-password.form.identification.header' | translate}}</div>
<div class="card-body">
<div class="row">
<div class="col-12">
<label class="font-weight-bold"
for="email">{{'forgot-password.form.identification.email' | translate}}</label>
<span id="email">{{(registration$ |async).email}}</span></div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">{{'forgot-password.form.card.security' | translate}}</div>
<div class="card-body">
<ds-profile-page-security-form
[passwordCanBeEmpty]="false"
[FORM_PREFIX]="'forgot-password.form.'"
(isInvalid)="setInValid($event)"
(passwordValue)="setPasswordValue($event)"
></ds-profile-page-security-form>
</div>
</div>
<div class="row">
<div class="col-12">
<button
[disabled]="isInValid"
class="btn btn-default btn-primary"
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
</div>
</div>
</div>

View File

@@ -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<ForgotPasswordFormComponent>;
let router;
let route;
let ePersonDataService: EPersonDataService;
let notificationsService;
let store: Store<CoreState>;
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();
});
})
});

View File

@@ -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<Registration>;
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<CoreState>,
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
);
}
});
}
}
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -1,9 +1,10 @@
<div class="container-fluid mb-4">{{'profile.security.form.info' | translate}}</div> <div class="container-fluid mb-4">{{FORM_PREFIX + 'info' | translate}}</div>
<ds-form *ngIf="formModel" <ds-form *ngIf="formModel"
[formId]="'profile-page-security-form-id'" [formId]="FORM_PREFIX"
[formModel]="formModel" [formModel]="formModel"
[formGroup]="formGroup" [formGroup]="formGroup"
[displaySubmit]="false"> [displaySubmit]="false">
</ds-form> </ds-form>
<div class="container-fluid text-danger" *ngIf="formGroup.hasError('notLongEnough')">{{'profile.security.form.error.password-length' | translate}}</div> <div id="notLongEnough" class="container-fluid text-danger" *ngIf="formGroup.hasError('notLongEnough')">{{FORM_PREFIX + 'error.password-length' | translate}}</div>
<div class="container-fluid text-danger" *ngIf="formGroup.hasError('notSame')">{{'profile.security.form.error.matching-passwords' | translate}}</div> <div id="notSame" class="container-fluid text-danger" *ngIf="formGroup.hasError('notSame')">{{FORM_PREFIX + 'error.matching-passwords' | translate}}</div>
<div id="emptyPassword" class="container-fluid text-danger" *ngIf="(formGroup.dirty || formGroup.touched) && formGroup.hasError('emptyPassword')">{{FORM_PREFIX + 'error.empty-password' | translate}}</div>

View File

@@ -1,5 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -15,18 +14,10 @@ describe('ProfilePageSecurityFormComponent', () => {
let component: ProfilePageSecurityFormComponent; let component: ProfilePageSecurityFormComponent;
let fixture: ComponentFixture<ProfilePageSecurityFormComponent>; let fixture: ComponentFixture<ProfilePageSecurityFormComponent>;
let user;
let epersonService; let epersonService;
let notificationsService; let notificationsService;
function init() { function init() {
user = Object.assign(new EPerson(), {
_links: {
self: { href: 'user-selflink' }
}
});
epersonService = jasmine.createSpyObj('epersonService', { epersonService = jasmine.createSpyObj('epersonService', {
patch: observableOf(new RestResponse(true, 200, 'OK')) patch: observableOf(new RestResponse(true, 200, 'OK'))
}); });
@@ -43,8 +34,8 @@ describe('ProfilePageSecurityFormComponent', () => {
declarations: [ProfilePageSecurityFormComponent, VarDirective], declarations: [ProfilePageSecurityFormComponent, VarDirective],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
providers: [ providers: [
{ provide: EPersonDataService, useValue: epersonService }, {provide: EPersonDataService, useValue: epersonService},
{ provide: NotificationsService, useValue: notificationsService }, {provide: NotificationsService, useValue: notificationsService},
FormBuilderService FormBuilderService
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -54,65 +45,35 @@ describe('ProfilePageSecurityFormComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ProfilePageSecurityFormComponent); fixture = TestBed.createComponent(ProfilePageSecurityFormComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.user = user;
fixture.detectChanges(); fixture.detectChanges();
}); });
describe('updateSecurity', () => { describe('On value change', () => {
describe('when no values changed', () => { describe('when the password has changed', () => {
let result;
beforeEach(() => { beforeEach(() => {
result = component.updateSecurity(); component.formGroup.patchValue({password: 'password'});
component.formGroup.patchValue({passwordrepeat: 'password'});
}); });
it('should return false', () => { it('should emit the value and validity on password change with invalid validity', fakeAsync(() => {
expect(result).toEqual(false); spyOn(component.passwordValue, 'emit');
}); spyOn(component.isInvalid, 'emit');
component.formGroup.patchValue({password: 'new-password'});
it('should not call epersonService.patch', () => { tick(300);
expect(epersonService.patch).not.toHaveBeenCalled();
});
});
describe('when password is filled in, but the confirm field is empty', () => { expect(component.passwordValue.emit).toHaveBeenCalledWith('new-password');
let result; expect(component.isInvalid.emit).toHaveBeenCalledWith(true);
}));
beforeEach(() => { it('should emit the value on password change', fakeAsync(() => {
setModelValue('password', 'test'); spyOn(component.passwordValue, 'emit');
result = component.updateSecurity(); component.formGroup.patchValue({password: 'new-password'});
});
it('should return true', () => { tick(300);
expect(result).toEqual(true);
});
});
describe('when both password fields are filled in, long enough and equal', () => { expect(component.passwordValue.emit).toHaveBeenCalledWith('new-password');
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);
}); });
}); });
});
function setModelValue(id: string, value: string) {
component.formGroup.patchValue({
[id]: value
});
component.formGroup.markAllAsTouched();
}
}); });

View File

@@ -1,16 +1,12 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { import { DynamicFormControlModel, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core';
DynamicFormControlModel,
DynamicFormService,
DynamicInputModel
} from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { FormGroup } from '@angular/forms'; 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 { 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 { NotificationsService } from '../../shared/notifications/notifications.service';
import { debounceTime, map } from 'rxjs/operators';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'ds-profile-page-security-form', 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 * Displays a form containing a password field and a confirmation of the password
*/ */
export class ProfilePageSecurityFormComponent implements OnInit { 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<boolean>();
/**
* Emits the value of the password
*/
@Output() passwordValue = new EventEmitter<string>();
/** /**
* The form's input models * The form's input models
@@ -48,14 +49,17 @@ export class ProfilePageSecurityFormComponent implements OnInit {
formGroup: FormGroup; 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 * 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, constructor(protected formService: DynamicFormService,
protected translate: TranslateService, protected translate: TranslateService,
@@ -64,12 +68,35 @@ export class ProfilePageSecurityFormComponent implements OnInit {
} }
ngOnInit(): void { 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.updateFieldTranslations();
this.translate.onLangChange this.translate.onLangChange
.subscribe(() => { .subscribe(() => {
this.updateFieldTranslations(); 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() { updateFieldTranslations() {
this.formModel.forEach( this.formModel.forEach(
(fieldModel: DynamicInputModel) => { (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 pass = group.get('password').value;
const repeatPass = group.get('passwordrepeat').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) { checkPasswordLength(group: FormGroup) {
const pass = group.get('password').value; 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 * Checks if the password is empty
* * @param group The FormGroup to validate
* 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
*/ */
updateSecurity() { checkPasswordEmpty(group: FormGroup) {
const pass = this.formGroup.get('password').value; const pass = group.get('password').value;
const passEntered = isNotEmpty(pass); return isEmpty(pass) ? {emptyPassword: true} : null;
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
);
}
});
} }
return passEntered; /**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
} }
} }

View File

@@ -10,7 +10,11 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header">{{'profile.card.security' | translate}}</div> <div class="card-header">{{'profile.card.security' | translate}}</div>
<div class="card-body"> <div class="card-body">
<ds-profile-page-security-form [user]="user"></ds-profile-page-security-form> <ds-profile-page-security-form
[FORM_PREFIX]="'profile.security.form.'"
(isInvalid)="setInvalid($event)"
(passwordValue)="setPasswordValue($event)"
></ds-profile-page-security-form>
</div> </div>
</div> </div>
<button class="btn btn-outline-primary" (click)="updateProfile()">{{'profile.form.submit' | translate}}</button> <button class="btn btn-outline-primary" (click)="updateProfile()">{{'profile.form.submit' | translate}}</button>

View File

@@ -13,8 +13,9 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
import { authReducer } from '../core/auth/auth.reducer'; import { authReducer } from '../core/auth/auth.reducer';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { createPaginatedList } from '../shared/testing/utils.test'; 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 { AuthService } from '../core/auth/auth.service';
import { RestResponse } from '../core/cache/response.models';
describe('ProfilePageComponent', () => { describe('ProfilePageComponent', () => {
let component: ProfilePageComponent; let component: ProfilePageComponent;
@@ -40,10 +41,11 @@ describe('ProfilePageComponent', () => {
}; };
authService = jasmine.createSpyObj('authService', { authService = jasmine.createSpyObj('authService', {
getAuthenticatedUserFromStore: of(user) getAuthenticatedUserFromStore: observableOf(user)
}); });
epersonService = jasmine.createSpyObj('epersonService', { epersonService = jasmine.createSpyObj('epersonService', {
findById: createSuccessfulRemoteDataObject$(user) findById: createSuccessfulRemoteDataObject$(user),
patch: observableOf(Object.assign(new RestResponse(true, 200, 'Success')))
}); });
notificationsService = jasmine.createSpyObj('notificationsService', { notificationsService = jasmine.createSpyObj('notificationsService', {
success: {}, success: {},
@@ -84,9 +86,7 @@ describe('ProfilePageComponent', () => {
component.metadataForm = jasmine.createSpyObj('metadataForm', { component.metadataForm = jasmine.createSpyObj('metadataForm', {
updateProfile: false updateProfile: false
}); });
component.securityForm = jasmine.createSpyObj('securityForm', { spyOn(component, 'updateSecurity').and.returnValue(true);
updateSecurity: true
});
component.updateProfile(); component.updateProfile();
}); });
@@ -100,9 +100,6 @@ describe('ProfilePageComponent', () => {
component.metadataForm = jasmine.createSpyObj('metadataForm', { component.metadataForm = jasmine.createSpyObj('metadataForm', {
updateProfile: true updateProfile: true
}); });
component.securityForm = jasmine.createSpyObj('securityForm', {
updateSecurity: false
});
component.updateProfile(); component.updateProfile();
}); });
@@ -116,9 +113,6 @@ describe('ProfilePageComponent', () => {
component.metadataForm = jasmine.createSpyObj('metadataForm', { component.metadataForm = jasmine.createSpyObj('metadataForm', {
updateProfile: true updateProfile: true
}); });
component.securityForm = jasmine.createSpyObj('securityForm', {
updateSecurity: true
});
component.updateProfile(); component.updateProfile();
}); });
@@ -132,9 +126,6 @@ describe('ProfilePageComponent', () => {
component.metadataForm = jasmine.createSpyObj('metadataForm', { component.metadataForm = jasmine.createSpyObj('metadataForm', {
updateProfile: false updateProfile: false
}); });
component.securityForm = jasmine.createSpyObj('securityForm', {
updateSecurity: false
});
component.updateProfile(); 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);
});
});
});
}); });

View File

@@ -2,7 +2,6 @@ import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { EPerson } from '../core/eperson/models/eperson.model'; import { EPerson } from '../core/eperson/models/eperson.model';
import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component'; 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 { NotificationsService } from '../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Group } from '../core/eperson/models/group.model'; 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 { filter, switchMap, tap } from 'rxjs/operators';
import { EPersonDataService } from '../core/eperson/eperson-data.service'; import { EPersonDataService } from '../core/eperson/eperson-data.service';
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators'; 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 { followLink } from '../shared/utils/follow-link-config.model';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
import { ErrorResponse, RestResponse } from '../core/cache/response.models';
@Component({ @Component({
selector: 'ds-profile-page', selector: 'ds-profile-page',
@@ -26,15 +26,10 @@ export class ProfilePageComponent implements OnInit {
/** /**
* A reference to the metadata form component * 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 * The authenticated user as observable
*/
@ViewChild(ProfilePageSecurityFormComponent, { static: false }) securityForm: ProfilePageSecurityFormComponent;
/**
* The authenticated user
*/ */
user$: Observable<EPerson>; user$: Observable<EPerson>;
@@ -48,6 +43,26 @@ export class ProfilePageComponent implements OnInit {
*/ */
NOTIFICATIONS_PREFIX = 'profile.notifications.'; 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, constructor(private authService: AuthService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private translate: TranslateService, private translate: TranslateService,
@@ -59,7 +74,8 @@ export class ProfilePageComponent implements OnInit {
filter((user: EPerson) => hasValue(user.id)), filter((user: EPerson) => hasValue(user.id)),
switchMap((user: EPerson) => this.epersonService.findById(user.id, followLink('groups'))), switchMap((user: EPerson) => this.epersonService.findById(user.id, followLink('groups'))),
getAllSucceededRemoteData(), getAllSucceededRemoteData(),
getRemoteDataPayload() getRemoteDataPayload(),
tap((user: EPerson) => this.currentUser = user)
); );
this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups)); this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups));
} }
@@ -70,7 +86,7 @@ export class ProfilePageComponent implements OnInit {
*/ */
updateProfile() { updateProfile() {
const metadataChanged = this.metadataForm.updateProfile(); const metadataChanged = this.metadataForm.updateProfile();
const securityChanged = this.securityForm.updateSecurity(); const securityChanged = this.updateSecurity();
if (!metadataChanged && !securityChanged) { if (!metadataChanged && !securityChanged) {
this.notificationsService.warning( this.notificationsService.warning(
this.translate.instant(this.NOTIFICATIONS_PREFIX + 'warning.no-changes.title'), 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();
}
} }

View File

@@ -12,6 +12,9 @@ import { ProfilePageSecurityFormComponent } from './profile-page-security-form/p
CommonModule, CommonModule,
SharedModule SharedModule
], ],
exports: [
ProfilePageSecurityFormComponent
],
declarations: [ declarations: [
ProfilePageComponent, ProfilePageComponent,
ProfilePageMetadataFormComponent, ProfilePageMetadataFormComponent,

View File

@@ -0,0 +1,36 @@
<div class="container">
<h2>{{MESSAGE_PREFIX + '.header'|translate}}</h2>
<p>{{MESSAGE_PREFIX + '.info' | translate}}</p>
<form [class]="'ng-invalid'" [formGroup]="form" (ngSubmit)="register()">
<div class="form-group">
<div class="row">
<div class="col-12">
<label class="font-weight-bold"
for="email">{{MESSAGE_PREFIX + '.email' | translate}}</label>
<input [className]="(email.invalid) && (email.dirty || email.touched) ? 'form-control is-invalid' :'form-control'"
type="text" id="email" formControlName="email"/>
<div *ngIf="email.invalid && (email.dirty || email.touched)"
class="invalid-feedback show-feedback">
<span *ngIf="email.errors && email.errors.required">
{{ MESSAGE_PREFIX + '.email.error.required' | translate }}
</span>
<span *ngIf="email.errors && email.errors.pattern">
{{ MESSAGE_PREFIX + '.email.error.pattern' | translate }}
</span>
</div>
</div>
<div class="col-12">
{{MESSAGE_PREFIX + '.email.hint' |translate}}
</div>
</div>
</div>
</form>
<button class="btn btn-primary"
[disabled]="form.invalid"
(click)="register()">{{MESSAGE_PREFIX + '.submit'| translate}}</button>
</div>

View File

@@ -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<RegisterEmailFormComponent>;
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 valid when a 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();
});
});
});

View File

@@ -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');
}
}

View File

@@ -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 {
}

View File

@@ -9,7 +9,7 @@ describe('RegistrationResolver', () => {
let epersonRegistrationService: EpersonRegistrationService; let epersonRegistrationService: EpersonRegistrationService;
const token = 'test-token'; 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(() => { beforeEach(() => {
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
@@ -25,6 +25,7 @@ describe('RegistrationResolver', () => {
(resolved) => { (resolved) => {
expect(resolved.token).toEqual(token); expect(resolved.token).toEqual(token);
expect(resolved.email).toEqual('test@email.org'); expect(resolved.email).toEqual('test@email.org');
expect(resolved.user).toEqual('user-uuid');
} }
); );
}); });

View File

@@ -1,6 +1,8 @@
<div class="container"> <div class="container">
<h2>{{'register-page.create-profile.header' | translate}}</h2> <h3 class="mb-4">{{'register-page.create-profile.header' | translate}}</h3>
<h4>{{'register-page.create-profile.identification.header' | translate}}</h4> <div class="card mb-4">
<div class="card-header">{{'register-page.create-profile.identification.header' | translate}}</div>
<div class="card-body">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<label class="font-weight-bold" <label class="font-weight-bold"
@@ -12,8 +14,7 @@
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<label class="font-weight-bold" <label for="firstName">{{'register-page.create-profile.identification.first-name' | translate}}</label>
for="firstName">{{'register-page.create-profile.identification.first-name' | translate}}</label>
<input [className]="(firstName.invalid) && (firstName.dirty || firstName.touched) ? 'form-control is-invalid' :'form-control'" <input [className]="(firstName.invalid) && (firstName.dirty || firstName.touched) ? 'form-control is-invalid' :'form-control'"
type="text" id="firstName" formControlName="firstName"/> type="text" id="firstName" formControlName="firstName"/>
<div *ngIf="firstName.invalid && (firstName.dirty || firstName.touched)" <div *ngIf="firstName.invalid && (firstName.dirty || firstName.touched)"
@@ -27,7 +28,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<label class="font-weight-bold" <label
for="lastName">{{'register-page.create-profile.identification.last-name' | translate}}</label> for="lastName">{{'register-page.create-profile.identification.last-name' | translate}}</label>
<input <input
[className]="(lastName.invalid) && (lastName.dirty || lastName.touched) ? 'form-control is-invalid' :'form-control'" [className]="(lastName.invalid) && (lastName.dirty || lastName.touched) ? 'form-control is-invalid' :'form-control'"
@@ -42,14 +43,14 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<label class="font-weight-bold" <label
for="contactPhone">{{'register-page.create-profile.identification.contact' | translate}}</label> for="contactPhone">{{'register-page.create-profile.identification.contact' | translate}}</label>
<input class="form-control" id="contactPhone" formControlName="contactPhone"> <input class="form-control" id="contactPhone" formControlName="contactPhone">
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<label class="font-weight-bold" <label
for="language">{{'register-page.create-profile.identification.language' |translate}}</label> for="language">{{'register-page.create-profile.identification.language' |translate}}</label>
<select id="language" formControlName="language" class="form-control"> <select id="language" formControlName="language" class="form-control">
@@ -60,40 +61,29 @@
</div> </div>
</div> </div>
</form> </form>
</div>
</div>
<h4>{{'register-page.create-profile.security.header' | translate}}</h4> <div class="card mb-4">
<p>{{'register-page.create-profile.security.info' | translate}}</p> <div class="card-header">{{'register-page.create-profile.security.header' | translate}}</div>
<div class="card-body">
<form class="form-horizontal" [formGroup]="passwordForm" (ngSubmit)="submitEperson()"> <ds-profile-page-security-form
<div class="form-group"> [passwordCanBeEmpty]="false"
<div class="row"> [FORM_PREFIX]="'register-page.create-profile.security.'"
<div class="col-12"> (isInvalid)="setInValid($event)"
<label class="font-weight-bold" for="password">{{'register-page.create-profile.security.password' |translate}}</label> (passwordValue)="setPasswordValue($event)"
<input [type]="'password'" ></ds-profile-page-security-form>
[className]="(!(isValidPassWord$|async)) && (password.dirty || password.touched) && (confirmPassword.dirty || confirmPassword.touched) ? 'form-control is-invalid' :'form-control'"
type="text" id="password" formControlName="password"/>
</div> </div>
</div> </div>
<div class="row">
<div class="col-12">
<label class="font-weight-bold" for="confirmPassword">{{'register-page.create-profile.security.confirm-password' |translate}}</label>
<input [type]="'password'"
[className]="(!(isValidPassWord$|async)) && (password.dirty || password.touched) && (confirmPassword.dirty || confirmPassword.touched) ? 'form-control is-invalid' :'form-control'"
id="confirmPassword" formControlName="confirmPassword">
<div *ngIf="!(isValidPassWord$|async) && (password.dirty || password.touched) && (confirmPassword.dirty || confirmPassword.touched)"
class="invalid-feedback show-feedback">
{{ 'register-page.create-profile.security.password.error' | translate }}
</div>
</div>
</div>
</div>
</form>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<button <button
[disabled]=" !(isValidPassWord$|async) || userInfoForm.invalid" [disabled]="isInValidPassword || userInfoForm.invalid"
class="btn btn-default btn-primary" (click)="submitEperson()">{{'register-page.create-profile.submit' | translate}}</button> class="btn btn-default btn-primary"
(click)="submitEperson()">{{'register-page.create-profile.submit' | translate}}</button>
</div> </div>
</div> </div>

View File

@@ -105,15 +105,11 @@ describe('CreateProfileComponent', () => {
const lastName = fixture.debugElement.queryAll(By.css('input#lastName'))[0].nativeElement; const lastName = fixture.debugElement.queryAll(By.css('input#lastName'))[0].nativeElement;
const contactPhone = fixture.debugElement.queryAll(By.css('input#contactPhone'))[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 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(firstName).toBeDefined();
expect(lastName).toBeDefined(); expect(lastName).toBeDefined();
expect(contactPhone).toBeDefined(); expect(contactPhone).toBeDefined();
expect(language).toBeDefined(); expect(language).toBeDefined();
expect(password).toBeDefined();
expect(confirmPassword).toBeDefined();
}); });
}); });
@@ -124,8 +120,8 @@ describe('CreateProfileComponent', () => {
comp.lastName.patchValue('Last'); comp.lastName.patchValue('Last');
comp.contactPhone.patchValue('Phone'); comp.contactPhone.patchValue('Phone');
comp.language.patchValue('en'); comp.language.patchValue('en');
comp.password.patchValue('password'); comp.password = 'password';
comp.confirmPassword.patchValue('password'); comp.isInValidPassword = false;
comp.submitEperson(); comp.submitEperson();
@@ -143,8 +139,8 @@ describe('CreateProfileComponent', () => {
comp.lastName.patchValue('Last'); comp.lastName.patchValue('Last');
comp.contactPhone.patchValue('Phone'); comp.contactPhone.patchValue('Phone');
comp.language.patchValue('en'); comp.language.patchValue('en');
comp.password.patchValue('password'); comp.password = 'password';
comp.confirmPassword.patchValue('password'); comp.isInValidPassword = false;
comp.submitEperson(); comp.submitEperson();
@@ -153,5 +149,36 @@ describe('CreateProfileComponent', () => {
expect(router.navigate).not.toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled();
expect(notificationsService.error).toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled();
}); });
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')));
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 create 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();
});
}); });
}); });

View File

@@ -1,10 +1,9 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; 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 { Registration } from '../../core/shared/registration.model';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { ConfirmedValidator } from './confirmed.validator';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model'; 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 { AuthenticateAction } from '../../core/auth/auth.actions';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { environment } from '../../../environments/environment'; 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 * 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; email: string;
token: string; token: string;
userInfoForm: FormGroup; isInValidPassword = true;
passwordForm: FormGroup; password: string;
activeLangs: LangConfig[];
isValidPassWord$: Observable<boolean>; userInfoForm: FormGroup;
activeLangs: LangConfig[];
constructor( constructor(
private translateService: TranslateService, private translateService: TranslateService,
@@ -67,29 +67,23 @@ export class CreateProfileComponent implements OnInit {
language: new FormControl(''), 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() { get firstName() {
@@ -108,20 +102,12 @@ export class CreateProfileComponent implements OnInit {
return this.userInfoForm.get('language'); 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. * 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() { submitEperson() {
if (!(this.userInfoForm.invalid || this.passwordForm.invalid)) { if (!(this.userInfoForm.invalid || this.isInValidPassword)) {
const values = { const values = {
metadata: { metadata: {
'eperson.firstname': [ 'eperson.firstname': [
@@ -146,7 +132,7 @@ export class CreateProfileComponent implements OnInit {
] ]
}, },
email: this.email, email: this.email,
password: this.password.value, password: this.password,
canLogIn: true, canLogIn: true,
requireCertificate: false requireCertificate: false
}; };
@@ -156,14 +142,14 @@ export class CreateProfileComponent implements OnInit {
if (response.isSuccessful) { if (response.isSuccessful) {
this.notificationsService.success(this.translateService.get('register-page.create-profile.submit.success.head'), this.notificationsService.success(this.translateService.get('register-page.create-profile.submit.success.head'),
this.translateService.get('register-page.create-profile.submit.success.content')); 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']); this.router.navigate(['/home']);
} else { } else {
this.notificationsService.error(this.translateService.get('register-page.create-profile.submit.error.head'), this.notificationsService.error(this.translateService.get('register-page.create-profile.submit.error.head'),
this.translateService.get('register-page.create-profile.submit.error.content')); this.translateService.get('register-page.create-profile.submit.error.content'));
} }
}); });
}
}
}
}
} }

View File

@@ -1,36 +1,3 @@
<div class="container"> <ds-register-email-form
<h2>{{'register-page.registration.header'|translate}}</h2> [MESSAGE_PREFIX]="'register-page.registration'">
<p>{{'register-page.registration.info' | translate}}</p> </ds-register-email-form>
<form [class]="'ng-invalid'" [formGroup]="form" (ngSubmit)="register()">
<div class="form-group">
<div class="row">
<div class="col-12">
<label class="font-weight-bold"
for="email">{{'register-page.registration.email' | translate}}</label>
<input [className]="(email.invalid) && (email.dirty || email.touched) ? 'form-control is-invalid' :'form-control'"
type="text" id="email" formControlName="email"/>
<div *ngIf="email.invalid && (email.dirty || email.touched)"
class="invalid-feedback show-feedback">
<span *ngIf="email.errors && email.errors.required">
{{ 'register-page.registration.email.error.required' | translate }}
</span>
<span *ngIf="email.errors && email.errors.pattern">
{{ 'register-page.registration.email.error.pattern' | translate }}
</span>
</div>
</div>
<div class="col-12">
{{'register-page.registration.email.hint' |translate}}
</div>
</div>
</div>
</form>
<button class="btn btn-primary"
[disabled]="form.invalid"
(click)="register()">{{'register-page.registration.register'| translate}}</button>
</div>

View File

@@ -1,46 +1,19 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 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 { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RegisterEmailComponent } from './register-email.component'; 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', () => { describe('RegisterEmailComponent', () => {
let comp: RegisterEmailComponent; let comp: RegisterEmailComponent;
let fixture: ComponentFixture<RegisterEmailComponent>; let fixture: ComponentFixture<RegisterEmailComponent>;
let router;
let epersonRegistrationService: EpersonRegistrationService;
let notificationsService;
beforeEach(async(() => { beforeEach(async(() => {
router = new RouterStub();
notificationsService = new NotificationsServiceStub();
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
registerEmail: observableOf(new RestResponse(true, 200, 'Success'))
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule], imports: [CommonModule, TranslateModule.forRoot(), ReactiveFormsModule],
declarations: [RegisterEmailComponent], declarations: [RegisterEmailComponent],
providers: [
{provide: Router, useValue: router},
{provide: EpersonRegistrationService, useValue: epersonRegistrationService},
{provide: FormBuilder, useValue: new FormBuilder()},
{provide: NotificationsService, useValue: notificationsService},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents(); }).compileComponents();
})); }));
@@ -50,43 +23,8 @@ describe('RegisterEmailComponent', () => {
fixture.detectChanges(); 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(); it('should be defined', () => {
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org'); expect(comp).toBeDefined();
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();
});
}); });
}); });

View File

@@ -1,64 +1,12 @@
import { Component, OnInit } from '@angular/core'; import { Component } 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({ @Component({
selector: 'ds-register-email', selector: 'ds-register-email',
templateUrl: './register-email.component.html' 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 { export class RegisterEmailComponent {
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');
}
} }

View File

@@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { RegisterEmailComponent } from './register-email/register-email.component'; import { RegisterEmailComponent } from './register-email/register-email.component';
import { CreateProfileComponent } from './create-profile/create-profile.component'; import { CreateProfileComponent } from './create-profile/create-profile.component';
import { RegistrationResolver } from './registration.resolver';
import { ItemPageResolver } from '../+item-page/item-page.resolver'; import { ItemPageResolver } from '../+item-page/item-page.resolver';
import { RegistrationResolver } from '../register-email-form/registration.resolver';
@NgModule({ @NgModule({
imports: [ 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 { export class RegisterPageRoutingModule {
} }

View File

@@ -4,12 +4,16 @@ import { SharedModule } from '../shared/shared.module';
import { RegisterPageRoutingModule } from './register-page-routing.module'; import { RegisterPageRoutingModule } from './register-page-routing.module';
import { RegisterEmailComponent } from './register-email/register-email.component'; import { RegisterEmailComponent } from './register-email/register-email.component';
import { CreateProfileComponent } from './create-profile/create-profile.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({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
RegisterPageRoutingModule, RegisterPageRoutingModule,
RegisterEmailFormModule,
ProfilePageModule,
], ],
declarations: [ declarations: [
RegisterEmailComponent, RegisterEmailComponent,
@@ -19,6 +23,9 @@ import { CreateProfileComponent } from './create-profile/create-profile.componen
entryComponents: [] entryComponents: []
}) })
/**
* Module related to components used to register a new user
*/
export class RegisterPageModule { export class RegisterPageModule {
} }

View File

@@ -9,5 +9,5 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" [routerLink]="[getRegisterPath()]">{{"login.form.new-user" | translate}}</a> <a class="dropdown-item" [routerLink]="[getRegisterPath()]">{{"login.form.new-user" | translate}}</a>
<a class="dropdown-item" href="#">{{"login.form.forgot-password" | translate}}</a> <a class="dropdown-item" [routerLink]="[getForgotPath()]">{{"login.form.forgot-password" | translate}}</a>
</div> </div>

View File

@@ -8,7 +8,7 @@ import { AuthMethod } from '../../core/auth/models/auth.method';
import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors'; import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
import { CoreState } from '../../core/core.reducers'; import { CoreState } from '../../core/core.reducers';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { getRegisterPath } from '../../app-routing.module'; import { getForgotPasswordPath, getRegisterPath } from '../../app-routing.module';
/** /**
* /users/sign-in * /users/sign-in
@@ -86,4 +86,8 @@ export class LogInComponent implements OnInit, OnDestroy {
getRegisterPath() { getRegisterPath() {
return getRegisterPath(); return getRegisterPath();
} }
getForgotPath() {
return getForgotPasswordPath();
}
} }

View File

@@ -948,6 +948,62 @@
"footer.link.duraspace": "DuraSpace", "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": "Add",
"form.add-help": "Click here to add the current entry and to add another one", "form.add-help": "Click here to add the current entry and to add another one",
@@ -2080,11 +2136,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.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", "register-page.create-profile.submit": "Complete Registration",
@@ -2109,7 +2169,7 @@
"register-page.registration.email.hint": "This address will be verified and used as your login name.", "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", "register-page.registration.success.head": "Verification email sent",