diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index 2407249615..c2c2353ded 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -90,10 +90,12 @@ describe('EpersonRegistrationService', () => { const expected = service.searchByToken('test-token'); expect(expected).toBeObservable(cold('(a|)', { - a: Object.assign(new Registration(), { - email: registrationWithUser.email, - token: 'test-token', - user: registrationWithUser.user + a: jasmine.objectContaining({ + payload: Object.assign(new Registration(), { + email: registrationWithUser.email, + token: 'test-token', + user: registrationWithUser.user + }) }) })); }); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index adf01b0ce9..989a401733 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -79,7 +79,7 @@ export class EpersonRegistrationService { * Search a registration based on the provided token * @param token */ - searchByToken(token: string): Observable { + searchByToken(token: string): Observable> { const requestId = this.requestService.generateRequestId(); const href$ = this.getTokenSearchEndpoint(token).pipe( @@ -97,15 +97,14 @@ export class EpersonRegistrationService { }); return this.rdbService.buildSingle(href$).pipe( - skipWhile((rd: RemoteData) => rd.isStale), - getFirstSucceededRemoteData(), - map((restResponse: RemoteData) => { - return Object.assign(new Registration(), { - email: restResponse.payload.email, token: token, user: restResponse.payload.user - }); - }), + map((rd) => { + if (rd.hasSucceeded && hasValue(rd.payload)) { + return Object.assign(rd, { payload: Object.assign(rd.payload, { token }) }); + } else { + return rd; + } + }) ); - } } diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.html b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.html index 06a1909f00..5fb0006279 100644 --- a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.html +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.html @@ -1,4 +1,4 @@ -
+

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

{{'forgot-password.form.identification.header' | translate}}
@@ -33,4 +33,4 @@ (click)="submit()">{{'forgot-password.form.submit' | translate}}
-
\ No newline at end of file + diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts index 30048acc6d..e76b327881 100644 --- a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts @@ -16,7 +16,11 @@ 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'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; describe('ForgotPasswordFormComponent', () => { let comp: ForgotPasswordFormComponent; @@ -36,7 +40,7 @@ describe('ForgotPasswordFormComponent', () => { beforeEach(waitForAsync(() => { - route = {data: observableOf({registration: registration})}; + route = {data: observableOf({registration: createSuccessfulRemoteDataObject(registration)})}; router = new RouterStub(); notificationsService = new NotificationsServiceStub(); diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts index 707c70f19c..75a13ac7f9 100644 --- a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts @@ -11,7 +11,10 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../../core/core.reducers'; import { RemoteData } from '../../core/data/remote-data'; import { EPerson } from '../../core/eperson/models/eperson.model'; -import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../../core/shared/operators'; @Component({ selector: 'ds-forgot-password-form', @@ -48,7 +51,8 @@ export class ForgotPasswordFormComponent { ngOnInit(): void { this.registration$ = this.route.data.pipe( - map((data) => data.registration as Registration), + map((data) => data.registration as RemoteData), + getFirstSucceededRemoteDataPayload(), ); this.registration$.subscribe((registration: Registration) => { this.email = registration.email; diff --git a/src/app/forgot-password/forgot-password-routing.module.ts b/src/app/forgot-password/forgot-password-routing.module.ts index 002ef5bc4b..ee4e081d5f 100644 --- a/src/app/forgot-password/forgot-password-routing.module.ts +++ b/src/app/forgot-password/forgot-password-routing.module.ts @@ -1,9 +1,9 @@ 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 { ThemedForgotPasswordFormComponent } from './forgot-password-form/themed-forgot-password-form.component'; import { ThemedForgotEmailComponent } from './forgot-password-email/themed-forgot-email.component'; +import { RegistrationGuard } from '../register-page/registration.guard'; @NgModule({ imports: [ @@ -16,12 +16,11 @@ import { ThemedForgotEmailComponent } from './forgot-password-email/themed-forgo { path: ':token', component: ThemedForgotPasswordFormComponent, - resolve: {registration: RegistrationResolver} + canActivate: [ RegistrationGuard ], } ]) ], providers: [ - RegistrationResolver, ItemPageResolver, ] }) diff --git a/src/app/register-email-form/registration.resolver.spec.ts b/src/app/register-email-form/registration.resolver.spec.ts index 0e7dd10cc2..86b200018d 100644 --- a/src/app/register-email-form/registration.resolver.spec.ts +++ b/src/app/register-email-form/registration.resolver.spec.ts @@ -1,8 +1,8 @@ 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'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; describe('RegistrationResolver', () => { let resolver: RegistrationResolver; @@ -13,7 +13,7 @@ describe('RegistrationResolver', () => { beforeEach(() => { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { - searchByToken: observableOf(registration) + searchByToken: createSuccessfulRemoteDataObject$(registration) }); resolver = new RegistrationResolver(epersonRegistrationService); }); @@ -23,9 +23,9 @@ describe('RegistrationResolver', () => { .pipe(first()) .subscribe( (resolved) => { - expect(resolved.token).toEqual(token); - expect(resolved.email).toEqual('test@email.org'); - expect(resolved.user).toEqual('user-uuid'); + expect(resolved.payload.token).toEqual(token); + expect(resolved.payload.email).toEqual('test@email.org'); + expect(resolved.payload.user).toEqual('user-uuid'); done(); } ); diff --git a/src/app/register-email-form/registration.resolver.ts b/src/app/register-email-form/registration.resolver.ts index 64ca2ba9f4..498d029492 100644 --- a/src/app/register-email-form/registration.resolver.ts +++ b/src/app/register-email-form/registration.resolver.ts @@ -3,18 +3,22 @@ import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/r import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; import { Registration } from '../core/shared/registration.model'; import { Observable } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; @Injectable() /** * Resolver to resolve a Registration object based on the provided token */ -export class RegistrationResolver implements Resolve { +export class RegistrationResolver implements Resolve> { constructor(private epersonRegistrationService: EpersonRegistrationService) { } - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { const token = route.params.token; - return this.epersonRegistrationService.searchByToken(token); + return this.epersonRegistrationService.searchByToken(token).pipe( + getFirstCompletedRemoteData(), + ); } } diff --git a/src/app/register-page/create-profile/create-profile.component.html b/src/app/register-page/create-profile/create-profile.component.html index cfd6e7ab16..f56059ad69 100644 --- a/src/app/register-page/create-profile/create-profile.component.html +++ b/src/app/register-page/create-profile/create-profile.component.html @@ -1,4 +1,4 @@ -
+

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

{{'register-page.create-profile.identification.header' | translate}}
diff --git a/src/app/register-page/create-profile/create-profile.component.spec.ts b/src/app/register-page/create-profile/create-profile.component.spec.ts index 6371044229..743454efdc 100644 --- a/src/app/register-page/create-profile/create-profile.component.spec.ts +++ b/src/app/register-page/create-profile/create-profile.component.spec.ts @@ -21,7 +21,11 @@ import { END_USER_AGREEMENT_METADATA_FIELD, EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; describe('CreateProfileComponent', () => { let comp: CreateProfileComponent; @@ -106,7 +110,7 @@ describe('CreateProfileComponent', () => { }; epersonWithAgreement = Object.assign(new EPerson(), valuesWithAgreement); - route = {data: observableOf({registration: registration})}; + route = {data: observableOf({registration: createSuccessfulRemoteDataObject(registration)})}; router = new RouterStub(); notificationsService = new NotificationsServiceStub(); diff --git a/src/app/register-page/create-profile/create-profile.component.ts b/src/app/register-page/create-profile/create-profile.component.ts index 790e1d6fc5..2439cd07f8 100644 --- a/src/app/register-page/create-profile/create-profile.component.ts +++ b/src/app/register-page/create-profile/create-profile.component.ts @@ -19,7 +19,7 @@ import { END_USER_AGREEMENT_METADATA_FIELD, EndUserAgreementService } from '../../core/end-user-agreement/end-user-agreement.service'; -import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; /** * Component that renders the create profile page to be used by a user registering through a token @@ -56,7 +56,8 @@ export class CreateProfileComponent implements OnInit { ngOnInit(): void { this.registration$ = this.route.data.pipe( - map((data) => data.registration as Registration), + map((data) => data.registration as RemoteData), + getFirstSucceededRemoteDataPayload(), ); this.registration$.subscribe((registration: Registration) => { this.email = registration.email; diff --git a/src/app/register-page/register-page-routing.module.ts b/src/app/register-page/register-page-routing.module.ts index cffe2a7349..cd448d55ac 100644 --- a/src/app/register-page/register-page-routing.module.ts +++ b/src/app/register-page/register-page-routing.module.ts @@ -2,9 +2,9 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { RegisterEmailComponent } from './register-email/register-email.component'; import { ItemPageResolver } from '../item-page/item-page.resolver'; -import { RegistrationResolver } from '../register-email-form/registration.resolver'; import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard'; import { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component'; +import { RegistrationGuard } from './registration.guard'; @NgModule({ imports: [ @@ -17,13 +17,14 @@ import { ThemedCreateProfileComponent } from './create-profile/themed-create-pro { path: ':token', component: ThemedCreateProfileComponent, - resolve: {registration: RegistrationResolver}, - canActivate: [EndUserAgreementCookieGuard] + canActivate: [ + RegistrationGuard, + EndUserAgreementCookieGuard, + ], } ]) ], providers: [ - RegistrationResolver, ItemPageResolver ] }) diff --git a/src/app/register-page/registration.guard.spec.ts b/src/app/register-page/registration.guard.spec.ts new file mode 100644 index 0000000000..89eaff7a02 --- /dev/null +++ b/src/app/register-page/registration.guard.spec.ts @@ -0,0 +1,106 @@ +import { RegistrationGuard } from './registration.guard'; +import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../core/auth/auth.service'; +import { Location } from '@angular/common'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, +} from '../shared/remote-data.utils'; +import { Registration } from '../core/shared/registration.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RemoteData } from '../core/data/remote-data'; + +describe('RegistrationGuard', () => { + let guard: RegistrationGuard; + + let epersonRegistrationService: EpersonRegistrationService; + let router: Router; + let authService: AuthService; + + let registration: Registration; + let registrationRD: RemoteData; + let currentUrl: string; + + let startingRouteData: any; + let route: ActivatedRouteSnapshot; + let state: RouterStateSnapshot; + + beforeEach(() => { + registration = Object.assign(new Registration(), { + email: 'test@email.com', + token: 'testToken', + user: 'testUser', + }); + registrationRD = createSuccessfulRemoteDataObject(registration); + currentUrl = 'test-current-url'; + + startingRouteData = { + existingData: 'some-existing-data', + }; + route = Object.assign(new ActivatedRouteSnapshot(), { + data: Object.assign({}, startingRouteData), + params: { + token: 'testToken', + }, + }); + state = Object.assign({ + url: currentUrl, + }); + + epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { + searchByToken: observableOf(registrationRD), + }); + router = jasmine.createSpyObj('router', { + navigateByUrl: Promise.resolve(), + }, { + url: currentUrl, + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(false), + setRedirectUrl: {}, + }); + + guard = new RegistrationGuard(epersonRegistrationService, router, authService); + }); + + describe('canActivate', () => { + describe('when searchByToken returns a successful response', () => { + beforeEach(() => { + (epersonRegistrationService.searchByToken as jasmine.Spy).and.returnValue(observableOf(registrationRD)); + }); + + it('should return true', (done) => { + guard.canActivate(route, state).subscribe((result) => { + expect(result).toEqual(true); + done(); + }); + }); + + it('should add the response to the route\'s data', (done) => { + guard.canActivate(route, state).subscribe(() => { + expect(route.data).toEqual({ ...startingRouteData, registration: registrationRD }); + done(); + }); + }); + + it('should not redirect', (done) => { + guard.canActivate(route, state).subscribe(() => { + expect(router.navigateByUrl).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('when searchByToken returns a 404 response', () => { + beforeEach(() => { + (epersonRegistrationService.searchByToken as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Not Found', 404)); + }); + + it('should redirect', () => { + guard.canActivate(route, state).subscribe(); + expect(router.navigateByUrl).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/app/register-page/registration.guard.ts b/src/app/register-page/registration.guard.ts new file mode 100644 index 0000000000..36bbfa4252 --- /dev/null +++ b/src/app/register-page/registration.guard.ts @@ -0,0 +1,43 @@ +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; +import { AuthService } from '../core/auth/auth.service'; +import { map } from 'rxjs/operators'; +import { getFirstCompletedRemoteData, redirectOn4xx } from '../core/shared/operators'; +import { Location } from '@angular/common'; + +@Injectable({ + providedIn: 'root' +}) +/** + * A guard responsible for redirecting to 4xx pages upon retrieving a Registration object + * The guard also adds the resulting RemoteData object to the route's data for further usage in components + * The reason this is a guard and not a resolver, is because it has to run before the EndUserAgreementCookieGuard + */ +export class RegistrationGuard implements CanActivate { + constructor(private epersonRegistrationService: EpersonRegistrationService, + private router: Router, + private authService: AuthService) { + } + + /** + * Can the user activate the route? Returns true if the provided token resolves to an existing Registration, false if + * not. Redirects to 4xx page on 4xx error. Adds the resulting RemoteData object to the route's + * data.registration property + * @param route + * @param state + */ + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const token = route.params.token; + return this.epersonRegistrationService.searchByToken(token).pipe( + getFirstCompletedRemoteData(), + redirectOn4xx(this.router, this.authService), + map((rd) => { + route.data = { ...route.data, registration: rd }; + return rd.hasSucceeded; + }), + ); + } + +}