diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 0ba0851e4e..db0a09662b 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -45,6 +45,13 @@ export function getProfileModulePath() { return `/${PROFILE_MODULE_PATH}`; } +const REGISTER_PATH = 'register'; + +export function getRegisterPath() { + return `/${REGISTER_PATH}`; + +} + const WORKFLOW_ITEM_MODULE_PATH = 'workflowitems'; export function getWorkflowItemModulePath() { @@ -71,6 +78,7 @@ export function getDSOPath(dso: DSpaceObject): string { { path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' }, { 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: 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/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 9237c30db9..be4bdf2a26 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -3,7 +3,6 @@ import { Action } from '@ngrx/store'; // import type function import { type } from '../../shared/ngrx/type'; // import models -import { EPerson } from '../eperson/models/eperson.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthMethod } from './models/auth.method'; import { AuthStatus } from './models/auth-status.model'; @@ -31,9 +30,6 @@ export const AuthActionTypes = { LOG_OUT: type('dspace/auth/LOG_OUT'), LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'), LOG_OUT_SUCCESS: type('dspace/auth/LOG_OUT_SUCCESS'), - REGISTRATION: type('dspace/auth/REGISTRATION'), - REGISTRATION_ERROR: type('dspace/auth/REGISTRATION_ERROR'), - REGISTRATION_SUCCESS: type('dspace/auth/REGISTRATION_SUCCESS'), SET_REDIRECT_URL: type('dspace/auth/SET_REDIRECT_URL'), RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'), RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'), @@ -263,48 +259,6 @@ export class RetrieveTokenAction implements Action { public type: string = AuthActionTypes.RETRIEVE_TOKEN; } -/** - * Sign up. - * @class RegistrationAction - * @implements {Action} - */ -export class RegistrationAction implements Action { - public type: string = AuthActionTypes.REGISTRATION; - payload: EPerson; - - constructor(user: EPerson) { - this.payload = user; - } -} - -/** - * Sign up error. - * @class RegistrationErrorAction - * @implements {Action} - */ -export class RegistrationErrorAction implements Action { - public type: string = AuthActionTypes.REGISTRATION_ERROR; - payload: Error; - - constructor(payload: Error) { - this.payload = payload; - } -} - -/** - * Sign up success. - * @class RegistrationSuccessAction - * @implements {Action} - */ -export class RegistrationSuccessAction implements Action { - public type: string = AuthActionTypes.REGISTRATION_SUCCESS; - payload: EPerson; - - constructor(user: EPerson) { - this.payload = user; - } -} - /** * Add uthentication message. * @class AddAuthenticationMessageAction @@ -439,9 +393,6 @@ export type AuthActions | CheckAuthenticationTokenCookieAction | RedirectWhenAuthenticationIsRequiredAction | RedirectWhenTokenExpiredAction - | RegistrationAction - | RegistrationErrorAction - | RegistrationSuccessAction | AddAuthenticationMessageAction | RefreshTokenAction | RefreshTokenErrorAction diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 5591ffbe39..37ef3b79bc 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators'; +import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators'; // import @ngrx import { Actions, Effect, ofType } from '@ngrx/effects'; import { Action, select, Store } from '@ngrx/store'; @@ -30,9 +30,6 @@ import { RefreshTokenAction, RefreshTokenErrorAction, RefreshTokenSuccessAction, - RegistrationAction, - RegistrationErrorAction, - RegistrationSuccessAction, RetrieveAuthenticatedEpersonAction, RetrieveAuthenticatedEpersonErrorAction, RetrieveAuthenticatedEpersonSuccessAction, @@ -136,18 +133,6 @@ export class AuthEffects { }) ); - @Effect() - public createUser$: Observable = this.actions$.pipe( - ofType(AuthActionTypes.REGISTRATION), - debounceTime(500), // to remove when functionality is implemented - switchMap((action: RegistrationAction) => { - return this.authService.create(action.payload).pipe( - map((user: EPerson) => new RegistrationSuccessAction(user)), - catchError((error) => observableOf(new RegistrationErrorAction(error))) - ); - }) - ); - @Effect() public retrieveToken$: Observable = this.actions$.pipe( ofType(AuthActionTypes.RETRIEVE_TOKEN), diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 16990b35a8..34c8fe2b41 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -115,7 +115,6 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut }); case AuthActionTypes.AUTHENTICATE_ERROR: - case AuthActionTypes.REGISTRATION_ERROR: return Object.assign({}, state, { authenticated: false, authToken: undefined, @@ -157,18 +156,6 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut userId: undefined }); - case AuthActionTypes.REGISTRATION: - return Object.assign({}, state, { - authenticated: false, - authToken: undefined, - error: undefined, - loading: true, - info: undefined - }); - - case AuthActionTypes.REGISTRATION_SUCCESS: - return state; - case AuthActionTypes.REFRESH_TOKEN: return Object.assign({}, state, { refreshing: true, diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 588d9e2675..f9c1fc2cb9 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -270,18 +270,6 @@ export class AuthService { return observableOf(authMethods); } - /** - * Create a new user - * @returns {User} - */ - public create(user: EPerson): Observable { - // Normally you would do an HTTP request to POST the user - // details and then return the new user object - // but, let's just return the new user for this example. - // this._authenticated = true; - return observableOf(user); - } - /** * End session * @returns {Observable} diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index b40965dd0a..dc7af65c8d 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -12,6 +12,7 @@ import { DSpaceObject } from '../shared/dspace-object.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataField } from '../metadata/metadata-field.model'; import { ContentSource } from '../shared/content-source.model'; +import { Registration } from '../shared/registration.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -257,4 +258,17 @@ export class ContentSourceSuccessResponse extends RestResponse { super(true, statusCode, statusText); } } + +/** + * A successful response containing a Registration + */ +export class RegistrationSuccessResponse extends RestResponse { + constructor( + public registration: Registration, + public statusCode: number, + public statusText: string, + ) { + super(true, statusCode, statusText); + } +} /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 37b3bb51e2..300d33fda8 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -140,6 +140,7 @@ import { Version } from './shared/version.model'; import { VersionHistory } from './shared/version-history.model'; import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowAction } from './tasks/models/workflow-action-object.model'; +import { Registration } from './shared/registration.model'; import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; import { MetadataFieldDataService } from './data/metadata-field-data.service'; @@ -308,7 +309,8 @@ export const models = ExternalSourceEntry, Version, VersionHistory, - WorkflowAction + WorkflowAction, + Registration ]; @NgModule({ diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts new file mode 100644 index 0000000000..d8623e7046 --- /dev/null +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -0,0 +1,84 @@ +import { RequestService } from './request.service'; +import { EpersonRegistrationService } from './eperson-registration.service'; +import { RegistrationSuccessResponse, RestResponse } from '../cache/response.models'; +import { RequestEntry } from './request.reducer'; +import { cold } from 'jasmine-marbles'; +import { PostRequest } from './request.models'; +import { Registration } from '../shared/registration.model'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; + +describe('EpersonRegistrationService', () => { + let service: EpersonRegistrationService; + let requestService: RequestService; + + let halService: any; + + const registration = new Registration(); + registration.email = 'test@mail.org'; + + beforeEach(() => { + halService = new HALEndpointServiceStub('rest-url'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: 'request-id', + configure: {}, + getByUUID: cold('a', + {a: Object.assign(new RequestEntry(), {response: new RestResponse(true, 200, 'Success')})}) + }); + service = new EpersonRegistrationService( + requestService, + halService + ); + }); + + describe('getRegistrationEndpoint', () => { + it('should retrieve the registration endpoint', () => { + const expected = service.getRegistrationEndpoint(); + + expected.subscribe(((value) => { + expect(value).toEqual('rest-url/registrations'); + })); + }); + }); + + describe('getTokenSearchEndpoint', () => { + it('should return the token search endpoint for a specified token', () => { + const expected = service.getTokenSearchEndpoint('test-token'); + + expected.subscribe(((value) => { + expect(value).toEqual('rest-url/registrations/search/findByToken?token=test-token'); + })); + }); + }); + + describe('registerEmail', () => { + it('should send an email registration', () => { + + const expected = service.registerEmail('test@mail.org'); + + expect(requestService.configure).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); + expect(expected).toBeObservable(cold('a', {a: new RestResponse(true, 200, 'Success')})); + }); + }); + + describe('searchByToken', () => { + beforeEach(() => { + (requestService.getByUUID as jasmine.Spy).and.returnValue( + cold('a', + {a: Object.assign(new RequestEntry(), {response: new RegistrationSuccessResponse(registration, 200, 'Success')})}) + ); + }); + it('should return a registration corresponding to the provided token', () => { + const expected = service.searchByToken('test-token'); + + expect(expected).toBeObservable(cold('(a|)', { + a: Object.assign(new Registration(), { + email: registration.email, + token: 'test-token' + }) + })); + + }); + }); + +}); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts new file mode 100644 index 0000000000..bc5df87288 --- /dev/null +++ b/src/app/core/data/eperson-registration.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { RequestService } from './request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { GetRequest, PostRequest } from './request.models'; +import { Observable } from 'rxjs'; +import { filter, find, map, take } from 'rxjs/operators'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { Registration } from '../shared/registration.model'; +import { filterSuccessfulResponses, getResponseFromEntry } from '../shared/operators'; +import { ResponseParsingService } from './parsing.service'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { RegistrationResponseParsingService } from './registration-response-parsing.service'; +import { RegistrationSuccessResponse } from '../cache/response.models'; + +@Injectable( + { + providedIn: 'root', + } +) +/** + * Service that will register a new email address and request a token + */ +export class EpersonRegistrationService { + + protected linkPath = 'registrations'; + protected searchByTokenPath = '/search/findByToken?token='; + + constructor( + protected requestService: RequestService, + protected halService: HALEndpointService, + ) { + + } + + /** + * Retrieves the Registration endpoint + */ + getRegistrationEndpoint(): Observable { + return this.halService.getEndpoint(this.linkPath); + } + + /** + * Retrieves the endpoint to search by registration token + */ + getTokenSearchEndpoint(token: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + filter((href: string) => isNotEmpty(href)), + map((href: string) => `${href}${this.searchByTokenPath}${token}`)); + } + + /** + * Register a new email address + * @param email + */ + registerEmail(email: string) { + const registration = new Registration(); + registration.email = email; + + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.getRegistrationEndpoint(); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PostRequest(requestId, href, registration); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + } + + /** + * Search a registration based on the provided token + * @param token + */ + searchByToken(token: string): Observable { + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.getTokenSearchEndpoint(token); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new GetRequest(requestId, href); + Object.assign(request, { + getResponseParser(): GenericConstructor { + return RegistrationResponseParsingService; + } + }); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + filterSuccessfulResponses(), + map((restResponse: RegistrationSuccessResponse) => { + return Object.assign(new Registration(), {email: restResponse.registration.email, token: token}); + }), + take(1), + ); + + } + +} diff --git a/src/app/core/data/registration-response-parsing.service.spec.ts b/src/app/core/data/registration-response-parsing.service.spec.ts new file mode 100644 index 0000000000..52044798a3 --- /dev/null +++ b/src/app/core/data/registration-response-parsing.service.spec.ts @@ -0,0 +1,22 @@ +import { Registration } from '../shared/registration.model'; +import { RegistrationResponseParsingService } from './registration-response-parsing.service'; +import { RegistrationSuccessResponse } from '../cache/response.models'; + +describe('RegistrationResponseParsingService', () => { + describe('parse', () => { + const registration = Object.assign(new Registration(), {email: 'test@email.org', token: 'test-token'}); + const registrationResponseParsingService = new RegistrationResponseParsingService(); + + const data = { + payload: {email: 'test@email.org', token: 'test-token'}, + statusCode: 200, + statusText: 'Success' + }; + + it('should parse a registration response', () => { + const expected = registrationResponseParsingService.parse({} as any, data); + + expect(expected).toEqual(new RegistrationSuccessResponse(registration, 200, 'Success')); + }); + }); +}); diff --git a/src/app/core/data/registration-response-parsing.service.ts b/src/app/core/data/registration-response-parsing.service.ts new file mode 100644 index 0000000000..8f5c35a3e1 --- /dev/null +++ b/src/app/core/data/registration-response-parsing.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { RegistrationSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { Registration } from '../shared/registration.model'; + +@Injectable({ + providedIn: 'root', +}) +/** + * Parsing service responsible for parsing a Registration response + */ +export class RegistrationResponseParsingService implements ResponseParsingService { + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + const payload = data.payload; + + const registration = Object.assign(new Registration(), payload); + + return new RegistrationSuccessResponse(registration, data.statusCode, data.statusText); + } + +} diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index 0cb56f14a2..1dd4e8dbd3 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -14,7 +14,7 @@ import { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; -import { DeleteByIDRequest, FindListOptions, PatchRequest } from '../data/request.models'; +import { DeleteByIDRequest, FindListOptions, PatchRequest, PostRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -291,6 +291,15 @@ describe('EPersonDataService', () => { }); }); + describe('createEPersonForToken', () => { + it('should sent a postRquest with an eperson to the token endpoint', () => { + service.createEPersonForToken(EPersonMock, 'test-token'); + + const expected = new PostRequest(requestService.generateRequestId(), epersonsEndpoint + '?token=test-token', EPersonMock); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + }); function getRemotedataObservable(obj: any): Observable> { diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index 86e53178a0..672793bc20 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { createSelector, select, Store } from '@ngrx/store'; import { Operation } from 'fast-json-patch/lib/core'; import { Observable } from 'rxjs'; -import { filter, map, take } from 'rxjs/operators'; +import { filter, find, map, take } from 'rxjs/operators'; import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction @@ -22,7 +22,7 @@ import { DataService } from '../data/data.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; -import { FindListOptions, FindListRequest, PatchRequest, } from '../data/request.models'; +import { FindListOptions, FindListRequest, PatchRequest, PostRequest, } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; @@ -165,17 +165,17 @@ export class EPersonDataService extends DataService { if (hasValue(oldEPerson.email) && oldEPerson.email !== newEPerson.email) { operations = [...operations, { op: 'replace', path: '/email', value: newEPerson.email - }] + }]; } if (hasValue(oldEPerson.requireCertificate) && oldEPerson.requireCertificate !== newEPerson.requireCertificate) { operations = [...operations, { op: 'replace', path: '/certificate', value: newEPerson.requireCertificate - }] + }]; } if (hasValue(oldEPerson.canLogIn) && oldEPerson.canLogIn !== newEPerson.canLogIn) { operations = [...operations, { op: 'replace', path: '/canLogIn', value: newEPerson.canLogIn - }] + }]; } return operations; } @@ -200,7 +200,7 @@ export class EPersonDataService extends DataService { * Method to retrieve the eperson that is currently being edited */ public getActiveEPerson(): Observable { - return this.store.pipe(select(editEPersonSelector)) + return this.store.pipe(select(editEPersonSelector)); } /** @@ -249,4 +249,25 @@ export class EPersonDataService extends DataService { return '/admin/access-control/epeople'; } + /** + * Create a new EPerson using a token + * @param eperson + * @param token + */ + public createEPersonForToken(eperson: EPerson, token: string) { + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getBrowseEndpoint().pipe( + map((href: string) => `${href}?token=${token}`)); + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PostRequest(requestId, href, eperson); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.fetchResponse(requestId); + + } + } diff --git a/src/app/core/eperson/models/eperson.model.ts b/src/app/core/eperson/models/eperson.model.ts index bb99022112..86202f1a55 100644 --- a/src/app/core/eperson/models/eperson.model.ts +++ b/src/app/core/eperson/models/eperson.model.ts @@ -57,6 +57,12 @@ export class EPerson extends DSpaceObject { @autoserialize public selfRegistered: boolean; + /** + * The password of this EPerson + */ + @autoserialize + public password: string; + /** * Getter to retrieve the EPerson's full name as a string */ diff --git a/src/app/core/shared/registration.model.ts b/src/app/core/shared/registration.model.ts new file mode 100644 index 0000000000..1943be2db5 --- /dev/null +++ b/src/app/core/shared/registration.model.ts @@ -0,0 +1,26 @@ +/** + * Model representing a registration + */ +export class Registration { + + /** + * The object type + */ + type: string; + + /** + * The email linked to the registration + */ + email: string; + + /** + * The user linked to the registration + */ + user: string; + + /** + * The token linked to the registration + */ + token: string; + +} diff --git a/src/app/register-page/create-profile/confirmed.validator.spec.ts b/src/app/register-page/create-profile/confirmed.validator.spec.ts new file mode 100644 index 0000000000..9bbbda9f29 --- /dev/null +++ b/src/app/register-page/create-profile/confirmed.validator.spec.ts @@ -0,0 +1,32 @@ +import { FormBuilder, FormControl } from '@angular/forms'; +import { async, fakeAsync } from '@angular/core/testing'; +import { ConfirmedValidator } from './confirmed.validator'; + +describe('ConfirmedValidator', () => { + let passwordForm; + + beforeEach(async(() => { + + passwordForm = (new FormBuilder()).group({ + password: new FormControl('', {}), + confirmPassword: new FormControl('', {}) + }, { + validator: ConfirmedValidator('password', 'confirmPassword') + }); + })); + + it('should validate that the password and confirm password match', fakeAsync(() => { + + passwordForm.get('password').patchValue('test-password'); + passwordForm.get('confirmPassword').patchValue('test-password-mismatch'); + + expect(passwordForm.valid).toBe(false); + })); + + it('should invalidate that the password and confirm password match', fakeAsync(() => { + passwordForm.get('password').patchValue('test-password'); + passwordForm.get('confirmPassword').patchValue('test-password'); + + expect(passwordForm.valid).toBe(true); + })); +}); diff --git a/src/app/register-page/create-profile/confirmed.validator.ts b/src/app/register-page/create-profile/confirmed.validator.ts new file mode 100644 index 0000000000..30f4f01701 --- /dev/null +++ b/src/app/register-page/create-profile/confirmed.validator.ts @@ -0,0 +1,19 @@ +import { FormGroup } from '@angular/forms'; + +/** + * Validator used to confirm that the password and confirmed password value are the same + */ +export function ConfirmedValidator(controlName: string, matchingControlName: string) { + return (formGroup: FormGroup) => { + const control = formGroup.controls[controlName]; + const matchingControl = formGroup.controls[matchingControlName]; + if (matchingControl.errors && !matchingControl.errors.confirmedValidator) { + return; + } + if (control.value !== matchingControl.value) { + matchingControl.setErrors({confirmedValidator: true}); + } else { + matchingControl.setErrors(null); + } + }; +} diff --git a/src/app/register-page/create-profile/create-profile.component.html b/src/app/register-page/create-profile/create-profile.component.html new file mode 100644 index 0000000000..dc6a3ddeef --- /dev/null +++ b/src/app/register-page/create-profile/create-profile.component.html @@ -0,0 +1,101 @@ +
+

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

+

{{'register-page.create-profile.identification.header' | 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.password.error' | 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 new file mode 100644 index 0000000000..5d509866a9 --- /dev/null +++ b/src/app/register-page/create-profile/create-profile.component.spec.ts @@ -0,0 +1,157 @@ +import { CreateProfileComponent } from './create-profile.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Registration } from '../../core/shared/registration.model'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { of as observableOf } from 'rxjs'; +import { RestResponse } from '../../core/cache/response.models'; +import { By } from '@angular/platform-browser'; +import { CoreState } from '../../core/core.reducers'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { AuthenticateAction } from '../../core/auth/auth.actions'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; + +describe('CreateProfileComponent', () => { + let comp: CreateProfileComponent; + 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', token: 'test-token'}); + + const values = { + metadata: { + 'eperson.firstname': [ + { + value: 'First' + } + ], + 'eperson.lastname': [ + { + value: 'Last' + }, + ], + 'eperson.phone': [ + { + value: 'Phone' + } + ], + 'eperson.language': [ + { + value: 'en' + } + ] + }, + email: 'test@email.org', + password: 'password', + canLogIn: true, + requireCertificate: false + }; + const eperson = Object.assign(new EPerson(), values); + + beforeEach(async(() => { + route = {data: observableOf({registration: registration})}; + router = new RouterStub(); + notificationsService = new NotificationsServiceStub(); + + ePersonDataService = jasmine.createSpyObj('ePersonDataService', { + createEPersonForToken: observableOf(new RestResponse(true, 200, 'Success')) + }); + + store = jasmine.createSpyObj('store', { + dispatch: {}, + }); + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule], + declarations: [CreateProfileComponent], + 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(CreateProfileComponent); + 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'); + }); + it('should initialise the form', () => { + const firstName = fixture.debugElement.queryAll(By.css('input#firstName'))[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 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(); + }); + }); + + describe('submitEperson', () => { + + it('should submit an eperson for creation and log in on success', () => { + comp.firstName.patchValue('First'); + comp.lastName.patchValue('Last'); + comp.contactPhone.patchValue('Phone'); + comp.language.patchValue('en'); + comp.password.patchValue('password'); + comp.confirmPassword.patchValue('password'); + + comp.submitEperson(); + + expect(ePersonDataService.createEPersonForToken).toHaveBeenCalledWith(eperson, 'test-token'); + expect(store.dispatch).toHaveBeenCalledWith(new AuthenticateAction('test@email.org', 'password')); + expect(router.navigate).toHaveBeenCalledWith(['/home']); + expect(notificationsService.success).toHaveBeenCalled(); + }); + + it('should submit an eperson for creation and stay on page on error', () => { + + (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.patchValue('password'); + comp.confirmPassword.patchValue('password'); + + comp.submitEperson(); + + expect(ePersonDataService.createEPersonForToken).toHaveBeenCalledWith(eperson, 'test-token'); + expect(store.dispatch).not.toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + expect(notificationsService.error).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 new file mode 100644 index 0000000000..aee9cd598d --- /dev/null +++ b/src/app/register-page/create-profile/create-profile.component.ts @@ -0,0 +1,169 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { debounceTime, 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'; +import { LangConfig } from '../../../config/lang-config.interface'; +import { Store } from '@ngrx/store'; +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'; + +/** + * Component that renders the create profile page to be used by a user registering through a token + */ +@Component({ + selector: 'ds-create-profile', + templateUrl: './create-profile.component.html' +}) +export class CreateProfileComponent implements OnInit { + registration$: Observable; + + email: string; + token: string; + + userInfoForm: FormGroup; + passwordForm: FormGroup; + activeLangs: LangConfig[]; + + isValidPassWord$: Observable; + + constructor( + private translateService: TranslateService, + private ePersonDataService: EPersonDataService, + private store: Store, + private router: Router, + private route: ActivatedRoute, + private formBuilder: FormBuilder, + private notificationsService: NotificationsService + ) { + + } + + 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.activeLangs = environment.languages.filter((MyLangConfig) => MyLangConfig.active === true); + + this.userInfoForm = this.formBuilder.group({ + firstName: new FormControl('', { + validators: [Validators.required], + }), + lastName: new FormControl('', { + validators: [Validators.required], + }), + contactPhone: 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; + } + }) + ); + } + + get firstName() { + return this.userInfoForm.get('firstName'); + } + + get lastName() { + return this.userInfoForm.get('lastName'); + } + + get contactPhone() { + return this.userInfoForm.get('contactPhone'); + } + + 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. + * The submission will not be made when the form is not valid. + */ + submitEperson() { + if (!(this.userInfoForm.invalid || this.passwordForm.invalid)) { + const values = { + metadata: { + 'eperson.firstname': [ + { + value: this.firstName.value + } + ], + 'eperson.lastname': [ + { + value: this.lastName.value + }, + ], + 'eperson.phone': [ + { + value: this.contactPhone.value + } + ], + 'eperson.language': [ + { + value: this.language.value + } + ] + }, + email: this.email, + password: this.password.value, + canLogIn: true, + requireCertificate: false + }; + + const eperson = Object.assign(new EPerson(), values); + this.ePersonDataService.createEPersonForToken(eperson, this.token).subscribe((response) => { + 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.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 new file mode 100644 index 0000000000..f506ab8f5d --- /dev/null +++ b/src/app/register-page/register-email/register-email.component.html @@ -0,0 +1,36 @@ +
+

{{'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 new file mode 100644 index 0000000000..67986853ea --- /dev/null +++ b/src/app/register-page/register-email/register-email.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 { 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], + 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(); + })); + beforeEach(() => { + fixture = TestBed.createComponent(RegisterEmailComponent); + 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-page/register-email/register-email.component.ts b/src/app/register-page/register-email/register-email.component.ts new file mode 100644 index 0000000000..c23a7797a2 --- /dev/null +++ b/src/app/register-page/register-email/register-email.component.ts @@ -0,0 +1,64 @@ +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'; + +@Component({ + selector: 'ds-register-email', + templateUrl: './register-email.component.html' +}) +/** + * Component responsible the email registration step + */ +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'); + } + +} diff --git a/src/app/register-page/register-page-routing.module.ts b/src/app/register-page/register-page-routing.module.ts new file mode 100644 index 0000000000..ac0edd7e70 --- /dev/null +++ b/src/app/register-page/register-page-routing.module.ts @@ -0,0 +1,32 @@ +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'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: RegisterEmailComponent, + data: {title: 'register-email.title'}, + }, + { + path: ':token', + component: CreateProfileComponent, + resolve: {registration: RegistrationResolver} + } + ]) + ], + providers: [ + RegistrationResolver, + ItemPageResolver + ] +}) +/** + * This module defines the default component to load when navigating to the mydspace page path. + */ +export class RegisterPageRoutingModule { +} diff --git a/src/app/register-page/register-page.module.ts b/src/app/register-page/register-page.module.ts new file mode 100644 index 0000000000..c6c45c8a23 --- /dev/null +++ b/src/app/register-page/register-page.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +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'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + RegisterPageRoutingModule, + ], + declarations: [ + RegisterEmailComponent, + CreateProfileComponent + ], + providers: [], + entryComponents: [] +}) + +export class RegisterPageModule { + +} diff --git a/src/app/register-page/registration.resolver.spec.ts b/src/app/register-page/registration.resolver.spec.ts new file mode 100644 index 0000000000..94f7b37a4f --- /dev/null +++ b/src/app/register-page/registration.resolver.spec.ts @@ -0,0 +1,32 @@ +import { RegistrationResolver } from './registration.resolver'; +import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; +import { of as observableOf } from 'rxjs'; +import { Registration } from '../core/shared/registration.model'; +import { first } from 'rxjs/operators'; + +describe('RegistrationResolver', () => { + let resolver: RegistrationResolver; + let epersonRegistrationService: EpersonRegistrationService; + + const token = 'test-token'; + const registration = Object.assign(new Registration(), {email: 'test@email.org', token: token}); + + beforeEach(() => { + epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { + searchByToken: observableOf(registration) + }); + resolver = new RegistrationResolver(epersonRegistrationService); + }); + describe('resolve', () => { + it('should resolve a registration based on the token', () => { + resolver.resolve({params: {token: token}} as any, undefined) + .pipe(first()) + .subscribe( + (resolved) => { + expect(resolved.token).toEqual(token); + expect(resolved.email).toEqual('test@email.org'); + } + ); + }); + }); +}); diff --git a/src/app/register-page/registration.resolver.ts b/src/app/register-page/registration.resolver.ts new file mode 100644 index 0000000000..64ca2ba9f4 --- /dev/null +++ b/src/app/register-page/registration.resolver.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; +import { Registration } from '../core/shared/registration.model'; +import { Observable } from 'rxjs'; + +@Injectable() +/** + * Resolver to resolve a Registration object based on the provided token + */ +export class RegistrationResolver implements Resolve { + + constructor(private epersonRegistrationService: EpersonRegistrationService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const token = route.params.token; + return this.epersonRegistrationService.searchByToken(token); + } +} diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html index 8e23f00d9b..2d52bf79bb 100644 --- a/src/app/shared/log-in/log-in.component.html +++ b/src/app/shared/log-in/log-in.component.html @@ -8,6 +8,6 @@ - {{"login.form.new-user" | translate}} + {{"login.form.new-user" | translate}} {{"login.form.forgot-password" | translate}} diff --git a/src/app/shared/log-in/log-in.component.spec.ts b/src/app/shared/log-in/log-in.component.spec.ts index 0167d61686..a9a42bf3dd 100644 --- a/src/app/shared/log-in/log-in.component.spec.ts +++ b/src/app/shared/log-in/log-in.component.spec.ts @@ -1,7 +1,7 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { Store, StoreModule } from '@ngrx/store'; +import { StoreModule } from '@ngrx/store'; import { LogInComponent } from './log-in.component'; import { authReducer } from '../../core/auth/auth.reducer'; @@ -13,11 +13,11 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { SharedModule } from '../shared.module'; import { NativeWindowMockFactory } from '../mocks/mock-native-window-ref'; import { ActivatedRouteStub } from '../testing/active-router.stub'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RouterStub } from '../testing/router.stub'; +import { ActivatedRoute } from '@angular/router'; import { NativeWindowService } from '../../core/services/window.service'; import { provideMockStore } from '@ngrx/store/testing'; import { createTestComponent } from '../testing/utils.test'; +import { RouterTestingModule } from '@angular/router/testing'; describe('LogInComponent', () => { @@ -46,6 +46,7 @@ describe('LogInComponent', () => { strictActionImmutability: false } }), + RouterTestingModule, SharedModule, TranslateModule.forRoot() ], @@ -55,7 +56,7 @@ describe('LogInComponent', () => { providers: [ { provide: AuthService, useClass: AuthServiceStub }, { provide: NativeWindowService, useFactory: NativeWindowMockFactory }, - { provide: Router, useValue: new RouterStub() }, + // { provide: Router, useValue: new RouterStub() }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, provideMockStore({ initialState }), LogInComponent diff --git a/src/app/shared/log-in/log-in.component.ts b/src/app/shared/log-in/log-in.component.ts index 92350de442..6634389c26 100644 --- a/src/app/shared/log-in/log-in.component.ts +++ b/src/app/shared/log-in/log-in.component.ts @@ -8,6 +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'; /** * /users/sign-in @@ -82,4 +83,7 @@ export class LogInComponent implements OnInit, OnDestroy { this.alive = false; } + getRegisterPath() { + return getRegisterPath(); + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 3a79984ec3..2f26c41699 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2056,6 +2056,70 @@ "publication.search.title": "DSpace Angular :: Publication Search", + "register-email.title": "New user registration", + + "register-page.create-profile.header": "Create Profile", + + "register-page.create-profile.identification.header": "Identify", + + "register-page.create-profile.identification.email": "Email Address", + + "register-page.create-profile.identification.first-name": "First Name *", + + "register-page.create-profile.identification.first-name.error": "Please fill in a First Name", + + "register-page.create-profile.identification.last-name": "Last Name *", + + "register-page.create-profile.identification.last-name.error": "Please fill in a Last Name", + + "register-page.create-profile.identification.contact": "Contact Telephone", + + "register-page.create-profile.identification.language": "Language", + + "register-page.create-profile.security.header": "Security", + + "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.confirm-password": "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.submit": "Complete Registration", + + "register-page.create-profile.submit.error.content": "Something went wrong while registering a new user.", + + "register-page.create-profile.submit.error.head": "Registration failed", + + "register-page.create-profile.submit.success.content": "The registration was successful. You have been logged in as the created user.", + + "register-page.create-profile.submit.success.head": "Registration completed", + + + "register-page.registration.header": "New user registration", + + "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", + + "register-page.registration.email": "Email Address *", + + "register-page.registration.email.error.required": "Please fill in an email address", + + "register-page.registration.email.error.pattern": "Please fill in a valid email address", + + "register-page.registration.email.hint": "This address will be verified and used as your login name.", + + "register-page.registration.register": "Register", + + "register-page.registration.success.head": "Verification email sent", + + "register-page.registration.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", + + "register-page.registration.error.head": "Error when trying to register email", + + "register-page.registration.error.content": "An error occured when registering the following email address: {{ email }}", + + "relationships.isAuthorOf": "Authors",