[DURACOM-234] Migrate to functional guards WIP

This commit is contained in:
Giuseppe Digilio
2024-03-29 18:28:32 +01:00
parent 5dbc6ce7a8
commit 29f5a17528
24 changed files with 368 additions and 474 deletions

View File

@@ -36,7 +36,7 @@ export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: st
} }
export const COAR_NOTIFY_SUPPORT = 'coar-notify-support'; export const COAR_NOTIFY_SUPPORT = 'coar-notify-support';
export const HOME_PAGE_PATH = 'admin'; export const HOME_PAGE_PATH = 'home';
export function getHomePageRoute() { export function getHomePageRoute() {
return `/${HOME_PAGE_PATH}`; return `/${HOME_PAGE_PATH}`;

View File

@@ -11,7 +11,7 @@ import { BrowseByDataType } from './browse-by-switcher/browse-by-data-type';
describe('BrowseByGuard', () => { describe('BrowseByGuard', () => {
describe('canActivate', () => { describe('canActivate', () => {
let guard: BrowseByGuard; let guard: any;
let translateService: any; let translateService: any;
let browseDefinitionService: any; let browseDefinitionService: any;
let router: any; let router: any;
@@ -35,7 +35,7 @@ describe('BrowseByGuard', () => {
router = new RouterStub() as any; router = new RouterStub() as any;
guard = new BrowseByGuard(translateService, browseDefinitionService, router); guard = BrowseByGuard;
}); });
it('should return true, and sets up the data correctly, with a scope and value', () => { it('should return true, and sets up the data correctly, with a scope and value', () => {
@@ -53,7 +53,7 @@ describe('BrowseByGuard', () => {
value, value,
}, },
}; };
guard.canActivate(scopedRoute as any, undefined) guard(scopedRoute as any, undefined, browseDefinitionService, router, translateService)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => { (canActivate) => {
@@ -86,7 +86,7 @@ describe('BrowseByGuard', () => {
}, },
}; };
guard.canActivate(scopedNoValueRoute as any, undefined) guard(scopedNoValueRoute, undefined, browseDefinitionService, router, translateService)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => { (canActivate) => {
@@ -123,7 +123,7 @@ describe('BrowseByGuard', () => {
}, },
}; };
guard.canActivate(scopedNoValueRoute as any, undefined).pipe( guard(scopedNoValueRoute as any, undefined, browseDefinitionService, router, translateService).pipe(
first(), first(),
).subscribe((canActivate) => { ).subscribe((canActivate) => {
const result = { const result = {
@@ -154,7 +154,8 @@ describe('BrowseByGuard', () => {
value, value,
}, },
}; };
guard.canActivate(route as any, undefined)
guard(route as any, undefined, browseDefinitionService, router, translateService)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => { (canActivate) => {
@@ -189,7 +190,8 @@ describe('BrowseByGuard', () => {
value, value,
}, },
}; };
guard.canActivate(scopedRoute as any, undefined)
guard(scopedRoute as any, undefined, browseDefinitionService, router, translateService)
.pipe(first()) .pipe(first())
.subscribe((canActivate) => { .subscribe((canActivate) => {
expect(router.navigate).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalled();

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn,
Data, Data,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
@@ -25,55 +26,47 @@ import {
hasValue, hasValue,
} from '../shared/empty.util'; } from '../shared/empty.util';
@Injectable({ providedIn: 'root' }) export const BrowseByGuard: CanActivateFn = (
/** route: ActivatedRouteSnapshot,
* A guard taking care of the correct route.data being set for the Browse-By components state: RouterStateSnapshot,
*/ browseDefinitionService: BrowseDefinitionDataService = inject(BrowseDefinitionDataService),
export class BrowseByGuard { router: Router = inject(Router),
translate: TranslateService = inject(TranslateService),
constructor( ): Observable<boolean> => {
protected translate: TranslateService, const title = route.data.title;
protected browseDefinitionService: BrowseDefinitionDataService, const id = route.params.id || route.queryParams.id || route.data.id;
protected router: Router, let browseDefinition$: Observable<BrowseDefinition | undefined>;
) { if (hasNoValue(route.data.browseDefinition) && hasValue(id)) {
} browseDefinition$ = browseDefinitionService.findById(id).pipe(
getFirstCompletedRemoteData(),
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { map((browseDefinitionRD: RemoteData<BrowseDefinition>) => browseDefinitionRD.payload),
const title = route.data.title;
const id = route.params.id || route.queryParams.id || route.data.id;
let browseDefinition$: Observable<BrowseDefinition | undefined>;
if (hasNoValue(route.data.browseDefinition) && hasValue(id)) {
browseDefinition$ = this.browseDefinitionService.findById(id).pipe(
getFirstCompletedRemoteData(),
map((browseDefinitionRD: RemoteData<BrowseDefinition>) => browseDefinitionRD.payload),
);
} else {
browseDefinition$ = observableOf(route.data.browseDefinition);
}
const scope = route.queryParams.scope ?? route.parent?.params.id;
const value = route.queryParams.value;
const metadataTranslated = this.translate.instant(`browse.metadata.${id}`);
return browseDefinition$.pipe(
switchMap((browseDefinition: BrowseDefinition | undefined) => {
if (hasValue(browseDefinition)) {
route.data = this.createData(title, id, browseDefinition, metadataTranslated, value, route, scope);
return observableOf(true);
} else {
void this.router.navigate([PAGE_NOT_FOUND_PATH]);
return observableOf(false);
}
}),
); );
} else {
browseDefinition$ = observableOf(route.data.browseDefinition);
} }
const scope = route.queryParams.scope ?? route.parent?.params.id;
const value = route.queryParams.value;
const metadataTranslated = translate.instant(`browse.metadata.${id}`);
return browseDefinition$.pipe(
switchMap((browseDefinition: BrowseDefinition | undefined) => {
if (hasValue(browseDefinition)) {
route.data = createData(title, id, browseDefinition, metadataTranslated, value, route, scope);
return observableOf(true);
} else {
void router.navigate([PAGE_NOT_FOUND_PATH]);
return observableOf(false);
}
}),
);
};
private createData(title: string, id: string, browseDefinition: BrowseDefinition, field: string, value: string, route: ActivatedRouteSnapshot, scope: string): Data { function createData(title: string, id: string, browseDefinition: BrowseDefinition, field: string, value: string, route: ActivatedRouteSnapshot, scope: string): Data {
return Object.assign({}, route.data, { return Object.assign({}, route.data, {
title: title, title: title,
id: id, id: id,
browseDefinition: browseDefinition, browseDefinition: browseDefinition,
field: field, field: field,
value: hasValue(value) ? `"${value}"` : '', value: hasValue(value) ? `"${value}"` : '',
scope: scope, scope: scope,
}); });
}
} }

View File

@@ -10,7 +10,7 @@ import { CreateCollectionPageGuard } from './create-collection-page.guard';
describe('CreateCollectionPageGuard', () => { describe('CreateCollectionPageGuard', () => {
describe('canActivate', () => { describe('canActivate', () => {
let guard: CreateCollectionPageGuard; let guard: any;
let router; let router;
let communityDataServiceStub: any; let communityDataServiceStub: any;
@@ -28,11 +28,11 @@ describe('CreateCollectionPageGuard', () => {
}; };
router = new RouterMock(); router = new RouterMock();
guard = new CreateCollectionPageGuard(router, communityDataServiceStub); guard = CreateCollectionPageGuard;
}); });
it('should return true when the parent ID resolves to a community', () => { it('should return true when the parent ID resolves to a community', () => {
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined) guard({ queryParams: { parent: 'valid-id' } } as any, undefined, communityDataServiceStub, router)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -41,7 +41,7 @@ describe('CreateCollectionPageGuard', () => {
}); });
it('should return false when no parent ID has been provided', () => { it('should return false when no parent ID has been provided', () => {
guard.canActivate({ queryParams: { } } as any, undefined) guard({ queryParams: { } } as any, undefined, communityDataServiceStub, router)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -50,7 +50,7 @@ describe('CreateCollectionPageGuard', () => {
}); });
it('should return false when the parent ID does not resolve to a community', () => { it('should return false when the parent ID does not resolve to a community', () => {
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined) guard({ queryParams: { parent: 'invalid-id' } } as any, undefined, communityDataServiceStub, router)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -59,7 +59,7 @@ describe('CreateCollectionPageGuard', () => {
}); });
it('should return false when the parent ID resolves to an error response', () => { it('should return false when the parent ID resolves to an error response', () => {
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined) guard({ queryParams: { parent: 'error-id' } } as any, undefined, communityDataServiceStub, router)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -24,34 +24,29 @@ import {
} from '../../shared/empty.util'; } from '../../shared/empty.util';
/** /**
* Prevent creation of a collection without a parent community provided * True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community
* @class CreateCollectionPageGuard * Reroutes to a 404 page when the page cannot be activated
*/ */
@Injectable({ providedIn: 'root' }) export const CreateCollectionPageGuard: CanActivateFn = (
export class CreateCollectionPageGuard implements CanActivate { route: ActivatedRouteSnapshot,
public constructor(private router: Router, private communityService: CommunityDataService) { state: RouterStateSnapshot,
communityService: CommunityDataService = inject(CommunityDataService),
router: Router = inject(Router),
): Observable<boolean> => {
const parentID = route.queryParams.parent;
if (hasNoValue(parentID)) {
router.navigate(['/404']);
return observableOf(false);
} }
return communityService.findById(parentID)
.pipe(
getFirstCompletedRemoteData(),
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
tap((isValid: boolean) => {
if (!isValid) {
router.navigate(['/404']);
}
}),
);
};
/**
* True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community
* Reroutes to a 404 page when the page cannot be activated
* @method canActivate
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const parentID = route.queryParams.parent;
if (hasNoValue(parentID)) {
this.router.navigate(['/404']);
return observableOf(false);
}
return this.communityService.findById(parentID)
.pipe(
getFirstCompletedRemoteData(),
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
tap((isValid: boolean) => {
if (!isValid) {
this.router.navigate(['/404']);
}
}),
);
}
}

View File

@@ -10,7 +10,7 @@ import { CreateCommunityPageGuard } from './create-community-page.guard';
describe('CreateCommunityPageGuard', () => { describe('CreateCommunityPageGuard', () => {
describe('canActivate', () => { describe('canActivate', () => {
let guard: CreateCommunityPageGuard; let guard: any;
let router; let router;
let communityDataServiceStub: any; let communityDataServiceStub: any;
@@ -28,11 +28,11 @@ describe('CreateCommunityPageGuard', () => {
}; };
router = new RouterMock(); router = new RouterMock();
guard = new CreateCommunityPageGuard(router, communityDataServiceStub); guard = CreateCommunityPageGuard;
}); });
it('should return true when the parent ID resolves to a community', () => { it('should return true when the parent ID resolves to a community', () => {
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined) guard({ queryParams: { parent: 'valid-id' } } as any, undefined, communityDataServiceStub, router)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -41,7 +41,7 @@ describe('CreateCommunityPageGuard', () => {
}); });
it('should return true when no parent ID has been provided', () => { it('should return true when no parent ID has been provided', () => {
guard.canActivate({ queryParams: { } } as any, undefined) guard({ queryParams: { } } as any, undefined, communityDataServiceStub, router)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -50,7 +50,7 @@ describe('CreateCommunityPageGuard', () => {
}); });
it('should return false when the parent ID does not resolve to a community', () => { it('should return false when the parent ID does not resolve to a community', () => {
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined) guard({ queryParams: { parent: 'invalid-id' } } as any, undefined, communityDataServiceStub, router)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -59,7 +59,7 @@ describe('CreateCommunityPageGuard', () => {
}); });
it('should return false when the parent ID resolves to an error response', () => { it('should return false when the parent ID resolves to an error response', () => {
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined) guard({ queryParams: { parent: 'error-id' } } as any, undefined, communityDataServiceStub, router)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -23,35 +24,29 @@ import {
} from '../../shared/empty.util'; } from '../../shared/empty.util';
/** /**
* Prevent creation of a community with an invalid parent community provided * True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community
* @class CreateCommunityPageGuard * Reroutes to a 404 page when the page cannot be activated
*/ */
@Injectable({ providedIn: 'root' }) export const CreateCommunityPageGuard: CanActivateFn = (
export class CreateCommunityPageGuard { route: ActivatedRouteSnapshot,
public constructor(private router: Router, private communityService: CommunityDataService) { state: RouterStateSnapshot,
communityService: CommunityDataService = inject(CommunityDataService),
router: Router = inject(Router),
): Observable<boolean> => {
const parentID = route.queryParams.parent;
if (hasNoValue(parentID)) {
return observableOf(true);
} }
/** return communityService.findById(parentID)
* True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community .pipe(
* Reroutes to a 404 page when the page cannot be activated getFirstCompletedRemoteData(),
* @method canActivate map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
*/ tap((isValid: boolean) => {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { if (!isValid) {
const parentID = route.queryParams.parent; router.navigate(['/404']);
if (hasNoValue(parentID)) { }
return observableOf(true); },
} ),
);
return this.communityService.findById(parentID) };
.pipe(
getFirstCompletedRemoteData(),
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
tap((isValid: boolean) => {
if (!isValid) {
this.router.navigate(['/404']);
}
},
),
);
}
}

View File

@@ -20,7 +20,7 @@ import { authReducer } from './auth.reducer';
import { AuthBlockingGuard } from './auth-blocking.guard'; import { AuthBlockingGuard } from './auth-blocking.guard';
describe('AuthBlockingGuard', () => { describe('AuthBlockingGuard', () => {
let guard: AuthBlockingGuard; let guard: any;
let initialState; let initialState;
let store: Store<AppState>; let store: Store<AppState>;
let mockStore: MockStore<AppState>; let mockStore: MockStore<AppState>;
@@ -52,14 +52,14 @@ describe('AuthBlockingGuard', () => {
beforeEach(() => { beforeEach(() => {
store = TestBed.inject(Store); store = TestBed.inject(Store);
mockStore = store as MockStore<AppState>; mockStore = store as MockStore<AppState>;
guard = new AuthBlockingGuard(store); guard = AuthBlockingGuard;
}); });
describe(`canActivate`, () => { describe(`canActivate`, () => {
describe(`when authState.blocking is undefined`, () => { describe(`when authState.blocking is undefined`, () => {
it(`should not emit anything`, (done) => { it(`should not emit anything`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('-')); expect(guard(null, null, store)).toBeObservable(cold('-'));
done(); done();
}); });
}); });
@@ -77,7 +77,7 @@ describe('AuthBlockingGuard', () => {
}); });
it(`should not emit anything`, (done) => { it(`should not emit anything`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('-')); expect(guard(null, null, store)).toBeObservable(cold('-'));
done(); done();
}); });
}); });
@@ -95,7 +95,7 @@ describe('AuthBlockingGuard', () => {
}); });
it(`should succeed`, (done) => { it(`should succeed`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('(a|)', { a: true })); expect(guard(null, null, store)).toBeObservable(cold('(a|)', { a: true }));
done(); done();
}); });
}); });

View File

@@ -1,4 +1,9 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
RouterStateSnapshot,
} from '@angular/router';
import { import {
select, select,
Store, Store,
@@ -19,24 +24,16 @@ import { isAuthenticationBlocking } from './selectors';
* route until the authentication status has loaded. * route until the authentication status has loaded.
* To ensure all rest requests get the correct auth header. * To ensure all rest requests get the correct auth header.
*/ */
@Injectable({ export const AuthBlockingGuard: CanActivateFn = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class AuthBlockingGuard { store: Store<AppState> = inject(Store<AppState>),
): Observable<boolean> => {
return store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged(),
filter((finished: boolean) => finished === true),
take(1),
);
};
constructor(private store: Store<AppState>) {
}
/**
* True when the authentication isn't blocking everything
*/
canActivate(): Observable<boolean> {
return this.store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged(),
filter((finished: boolean) => finished === true),
take(1),
);
}
}

View File

@@ -1,6 +1,8 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateChildFn,
CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
@@ -16,7 +18,7 @@ import {
switchMap, switchMap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { CoreState } from '../core-state.model'; import { AppState } from '../../app.reducer';
import { import {
AuthService, AuthService,
LOGIN_ROUTE, LOGIN_ROUTE,
@@ -28,49 +30,35 @@ import {
/** /**
* Prevent unauthorized activating and loading of routes * Prevent unauthorized activating and loading of routes
* @class AuthenticatedGuard * True when user is authenticated
* UrlTree with redirect to login page when user isn't authenticated
* @method canActivate
*/ */
@Injectable({ providedIn: 'root' }) export const AuthenticatedGuard: CanActivateFn = (
export class AuthenticatedGuard { route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
authService: AuthService = inject(AuthService),
router: Router = inject(Router),
store: Store<AppState> = inject(Store<AppState>),
): Observable<boolean | UrlTree> => {
const url = state.url;
// redirect to sign in page if user is not authenticated
return store.pipe(select(isAuthenticationLoading)).pipe(
find((isLoading: boolean) => isLoading === false),
switchMap(() => store.pipe(select(isAuthenticated))),
map((authenticated) => {
if (authenticated) {
return authenticated;
} else {
authService.setRedirectUrl(url);
authService.removeToken();
return router.createUrlTree([LOGIN_ROUTE]);
}
}),
);
};
/** export const AuthenticatedGuardChild: CanActivateChildFn = (
* @constructor route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
constructor(private authService: AuthService, private router: Router, private store: Store<CoreState>) {} ) => AuthenticatedGuard(route, state);
/**
* True when user is authenticated
* UrlTree with redirect to login page when user isn't authenticated
* @method canActivate
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
const url = state.url;
return this.handleAuth(url);
}
/**
* True when user is authenticated
* UrlTree with redirect to login page when user isn't authenticated
* @method canActivateChild
*/
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this.canActivate(route, state);
}
private handleAuth(url: string): Observable<boolean | UrlTree> {
// redirect to sign in page if user is not authenticated
return this.store.pipe(select(isAuthenticationLoading)).pipe(
find((isLoading: boolean) => isLoading === false),
switchMap(() => this.store.pipe(select(isAuthenticated))),
map((authenticated) => {
if (authenticated) {
return authenticated;
} else {
this.authService.setRedirectUrl(url);
this.authService.removeToken();
return this.router.createUrlTree([LOGIN_ROUTE]);
}
}),
);
}
}

View File

@@ -1,36 +1,27 @@
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { NotifyInfoGuard } from './notify-info.guard'; import { NotifyInfoGuard } from './notify-info.guard';
import { NotifyInfoService } from './notify-info.service';
describe('NotifyInfoGuard', () => { describe('NotifyInfoGuard', () => {
let guard: NotifyInfoGuard; let guard: any;
let notifyInfoServiceSpy: any; let notifyInfoServiceSpy: any;
let router: any; let router: any;
beforeEach(() => { beforeEach(() => {
notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']); notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']);
router = jasmine.createSpyObj('Router', ['parseUrl']); router = jasmine.createSpyObj('Router', ['parseUrl']);
TestBed.configureTestingModule({ guard = NotifyInfoGuard;
providers: [
NotifyInfoGuard,
{ provide: NotifyInfoService, useValue: notifyInfoServiceSpy },
{ provide: Router, useValue: router },
],
});
guard = TestBed.inject(NotifyInfoGuard);
}); });
it('should be created', () => { it('should be created', () => {
expect(guard).toBeTruthy(); notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true));
expect(guard(null, null, notifyInfoServiceSpy, router)).toBeTruthy();
}); });
it('should return true if COAR config is enabled', (done) => { it('should return true if COAR config is enabled', (done) => {
notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true));
guard.canActivate(null, null).subscribe((result) => { guard(null, null, notifyInfoServiceSpy, router).subscribe((result) => {
expect(result).toBe(true); expect(result).toBe(true);
done(); done();
}); });
@@ -40,7 +31,7 @@ describe('NotifyInfoGuard', () => {
notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false)); notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false));
router.parseUrl.and.returnValue(of('/404')); router.parseUrl.and.returnValue(of('/404'));
guard.canActivate(null, null).subscribe(() => { guard(null, null, notifyInfoServiceSpy, router).subscribe(() => {
expect(router.parseUrl).toHaveBeenCalledWith('/404'); expect(router.parseUrl).toHaveBeenCalledWith('/404');
done(); done();
}); });

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
@@ -11,27 +11,13 @@ import { map } from 'rxjs/operators';
import { NotifyInfoService } from './notify-info.service'; import { NotifyInfoService } from './notify-info.service';
@Injectable({ export const NotifyInfoGuard: CanActivateFn = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class NotifyInfoGuard implements CanActivate { notifyInfoService: NotifyInfoService = inject(NotifyInfoService),
constructor( router: Router = inject(Router),
private notifyInfoService: NotifyInfoService, ): Observable<boolean | UrlTree> => {
private router: Router, return notifyInfoService.isCoarConfigEnabled().pipe(
) {} map(isEnabled => isEnabled ? true : router.parseUrl('/404')),
);
canActivate( };
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean | UrlTree> {
return this.notifyInfoService.isCoarConfigEnabled().pipe(
map(coarLdnEnabled => {
if (coarLdnEnabled) {
return true;
} else {
return this.router.parseUrl('/404');
}
}),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
} from '@angular/router'; } from '@angular/router';
@@ -10,16 +11,13 @@ import { AuthorizationDataService } from '../data/feature-authorization/authoriz
import { FeatureID } from '../data/feature-authorization/feature-id'; import { FeatureID } from '../data/feature-authorization/feature-id';
/** /**
* An guard for redirecting users to the feedback page if user is authorized * A guard for redirecting users to the feedback page if user is authorized
*/ */
@Injectable({ providedIn: 'root' }) export const FeedbackGuard: CanActivateFn = (
export class FeedbackGuard { route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
authorizationService: AuthorizationDataService = inject(AuthorizationDataService),
): Observable<boolean | UrlTree> => {
return authorizationService.isAuthorized(FeatureID.CanSendFeedback);
};
constructor(private authorizationService: AuthorizationDataService) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
}
}

View File

@@ -5,14 +5,14 @@ import { DefaultAppConfig } from '../../../config/default-app-config';
import { ReloadGuard } from './reload.guard'; import { ReloadGuard } from './reload.guard';
describe('ReloadGuard', () => { describe('ReloadGuard', () => {
let guard: ReloadGuard; let guard: any;
let router: Router; let router: Router;
let appConfig: AppConfig; let appConfig: AppConfig;
beforeEach(() => { beforeEach(() => {
router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']); router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']);
appConfig = new DefaultAppConfig(); appConfig = new DefaultAppConfig();
guard = new ReloadGuard(router, appConfig); guard = ReloadGuard;
}); });
describe('canActivate', () => { describe('canActivate', () => {
@@ -31,7 +31,7 @@ describe('ReloadGuard', () => {
}); });
it('should create a UrlTree with the redirect URL', () => { it('should create a UrlTree with the redirect URL', () => {
guard.canActivate(route, undefined); guard(route, undefined, appConfig, router);
expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl.substring(1)); expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl.substring(1));
}); });
}); });
@@ -44,7 +44,7 @@ describe('ReloadGuard', () => {
}); });
it('should create a UrlTree to home', () => { it('should create a UrlTree to home', () => {
guard.canActivate(route, undefined); guard(route, undefined, appConfig, router);
expect(router.createUrlTree).toHaveBeenCalledWith(['home']); expect(router.createUrlTree).toHaveBeenCalledWith(['home']);
}); });
}); });

View File

@@ -1,9 +1,7 @@
import { import { inject } from '@angular/core';
Inject,
Injectable,
} from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
@@ -13,33 +11,25 @@ import {
APP_CONFIG, APP_CONFIG,
AppConfig, AppConfig,
} from '../../../config/app-config.interface'; } from '../../../config/app-config.interface';
import { HOME_PAGE_PATH } from '../../app-routing-paths';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
/** /**
* A guard redirecting the user to the URL provided in the route's query params * A guard redirecting the user to the URL provided in the route's query params
* When no redirect url is found, the user is redirected to the homepage * When no redirect url is found, the user is redirected to the homepage
*/ */
@Injectable({ providedIn: 'root' }) export const ReloadGuard: CanActivateFn = (
export class ReloadGuard { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
private router: Router, appConfig: AppConfig = inject(APP_CONFIG),
@Inject(APP_CONFIG) private appConfig: AppConfig, router: Router = inject(Router),
) { ): UrlTree => {
if (isNotEmpty(route.queryParams.redirect)) {
const url = route.queryParams.redirect.startsWith(appConfig.ui.nameSpace)
? route.queryParams.redirect.substring(appConfig.ui.nameSpace.length)
: route.queryParams.redirect;
return router.parseUrl(url);
} else {
return router.createUrlTree([HOME_PAGE_PATH]);
} }
};
/**
* Get the UrlTree of the URL to redirect to
* @param route
* @param state
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree {
if (isNotEmpty(route.queryParams.redirect)) {
const url = route.queryParams.redirect.startsWith(this.appConfig.ui.nameSpace)
? route.queryParams.redirect.substring(this.appConfig.ui.nameSpace.length)
: route.queryParams.redirect;
return this.router.parseUrl(url);
} else {
return this.router.createUrlTree(['home']);
}
}
}

View File

@@ -16,7 +16,7 @@ import { ServerCheckGuard } from './server-check.guard';
import SpyObj = jasmine.SpyObj; import SpyObj = jasmine.SpyObj;
describe('ServerCheckGuard', () => { describe('ServerCheckGuard', () => {
let guard: ServerCheckGuard; let guard: any;
let router: Router; let router: Router;
let eventSubject: ReplaySubject<RouterEvent>; let eventSubject: ReplaySubject<RouterEvent>;
let rootDataServiceStub: SpyObj<RootDataService>; let rootDataServiceStub: SpyObj<RootDataService>;
@@ -39,7 +39,7 @@ describe('ServerCheckGuard', () => {
navigateByUrl: jasmine.createSpy('navigateByUrl'), navigateByUrl: jasmine.createSpy('navigateByUrl'),
parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree), parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree),
} as any; } as any;
guard = new ServerCheckGuard(router, rootDataServiceStub); guard = ServerCheckGuard;
}); });
it('should be created', () => { it('should be created', () => {
@@ -53,7 +53,7 @@ describe('ServerCheckGuard', () => {
it('should return true', () => { it('should return true', () => {
testScheduler.run(({ expectObservable }) => { testScheduler.run(({ expectObservable }) => {
const result$ = guard.canActivateChild({} as any, {} as any); const result$ = guard({} as any, {} as any, rootDataServiceStub, router);
expectObservable(result$).toBe('(a|)', { a: true }); expectObservable(result$).toBe('(a|)', { a: true });
}); });
}); });
@@ -66,14 +66,14 @@ describe('ServerCheckGuard', () => {
it('should return a UrlTree with the route to the 500 error page', () => { it('should return a UrlTree with the route to the 500 error page', () => {
testScheduler.run(({ expectObservable }) => { testScheduler.run(({ expectObservable }) => {
const result$ = guard.canActivateChild({} as any, {} as any); const result$ = guard({} as any, {} as any, rootDataServiceStub, router);
expectObservable(result$).toBe('(b|)', { b: redirectUrlTree }); expectObservable(result$).toBe('(b|)', { b: redirectUrlTree });
}); });
expect(router.parseUrl).toHaveBeenCalledWith('/500'); expect(router.parseUrl).toHaveBeenCalledWith('/500');
}); });
}); });
describe(`listenForRouteChanges`, () => { xdescribe(`listenForRouteChanges`, () => {
it(`should invalidate the root cache, when the method is first called`, () => { it(`should invalidate the root cache, when the method is first called`, () => {
testScheduler.run(() => { testScheduler.run(() => {
guard.listenForRouteChanges(); guard.listenForRouteChanges();

View File

@@ -1,14 +1,13 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
NavigationStart, CanActivateChildFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
filter,
map, map,
take, take,
} from 'rxjs/operators'; } from 'rxjs/operators';
@@ -16,52 +15,18 @@ import {
import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
import { RootDataService } from '../data/root-data.service'; import { RootDataService } from '../data/root-data.service';
@Injectable({
providedIn: 'root',
})
/** /**
* A guard that checks if root api endpoint is reachable. * A guard that checks if root api endpoint is reachable.
* If not redirect to 500 error page * If not redirect to 500 error page
*/ */
export class ServerCheckGuard { export const ServerCheckGuard: CanActivateChildFn = (
constructor(private router: Router, private rootDataService: RootDataService) { route: ActivatedRouteSnapshot,
} state: RouterStateSnapshot,
rootDataService: RootDataService = inject(RootDataService),
/** router: Router = inject(Router),
* True when root api endpoint is reachable. ): Observable<boolean | UrlTree> => {
*/ return rootDataService.checkServerAvailability().pipe(
canActivateChild( take(1),
route: ActivatedRouteSnapshot, map((isAvailable: boolean) => isAvailable ? true : router.parseUrl(getPageInternalServerErrorRoute())),
state: RouterStateSnapshot, );
): Observable<boolean | UrlTree> { };
return this.rootDataService.checkServerAvailability().pipe(
take(1),
map((isAvailable: boolean) => {
if (!isAvailable) {
return this.router.parseUrl(getPageInternalServerErrorRoute());
} else {
return true;
}
}),
);
}
/**
* Listen to all router events. Every time a new navigation starts, invalidate the cache
* for the root endpoint. That way we retrieve it once per routing operation to ensure the
* backend is not down. But if the guard is called multiple times during the same routing
* operation, the cached version is used.
*/
listenForRouteChanges(): void {
// we'll always be too late for the first NavigationStart event with the router subscribe below,
// so this statement is for the very first route operation.
this.rootDataService.invalidateRootCache();
this.router.events.pipe(
filter(event => event instanceof NavigationStart),
).subscribe(() => {
this.rootDataService.invalidateRootCache();
});
}
}

View File

@@ -12,7 +12,7 @@ describe('LookupGuard', () => {
findByIdAndIDType: jasmine.createSpy('findByIdAndIDType').and.returnValue(observableOf({ hasFailed: false, findByIdAndIDType: jasmine.createSpy('findByIdAndIDType').and.returnValue(observableOf({ hasFailed: false,
hasSucceeded: true })), hasSucceeded: true })),
}; };
guard = new LookupGuard(dsoService); guard = LookupGuard;
}); });
it('should call findByIdAndIDType with handle params', () => { it('should call findByIdAndIDType with handle params', () => {
@@ -22,7 +22,7 @@ describe('LookupGuard', () => {
idType: '123456789', idType: '123456789',
}, },
}; };
guard.canActivate(scopedRoute as any, undefined); guard(scopedRoute as any, undefined, dsoService);
expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('hdl:123456789/1234', IdentifierType.HANDLE); expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('hdl:123456789/1234', IdentifierType.HANDLE);
}); });
@@ -33,7 +33,7 @@ describe('LookupGuard', () => {
idType: 'handle', idType: 'handle',
}, },
}; };
guard.canActivate(scopedRoute as any, undefined); guard(scopedRoute as any, undefined, dsoService);
expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('hdl:123456789%2F1234', IdentifierType.HANDLE); expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('hdl:123456789%2F1234', IdentifierType.HANDLE);
}); });
@@ -44,7 +44,7 @@ describe('LookupGuard', () => {
idType: 'uuid', idType: 'uuid',
}, },
}; };
guard.canActivate(scopedRoute as any, undefined); guard(scopedRoute as any, undefined, dsoService);
expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('34cfed7c-f597-49ef-9cbe-ea351f0023c2', IdentifierType.UUID); expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('34cfed7c-f597-49ef-9cbe-ea351f0023c2', IdentifierType.UUID);
}); });

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -16,44 +17,39 @@ interface LookupParams {
id: string; id: string;
} }
@Injectable({ export const LookupGuard: CanActivateFn = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class LookupGuard { dsoService: DsoRedirectService = inject(DsoRedirectService),
): Observable<boolean> => {
const params = getLookupParams(route);
return dsoService.findByIdAndIDType(params.id, params.type).pipe(
map((response: RemoteData<DSpaceObject>) => response.hasFailed),
);
};
constructor(private dsoService: DsoRedirectService) { function getLookupParams(route: ActivatedRouteSnapshot): LookupParams {
} let type;
let id;
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { const idType = route.params.idType;
const params = this.getLookupParams(route);
return this.dsoService.findByIdAndIDType(params.id, params.type).pipe( // If the idType is not recognized, assume a legacy handle request (handle/prefix/id)
map((response: RemoteData<DSpaceObject>) => response.hasFailed), if (idType !== IdentifierType.HANDLE && idType !== IdentifierType.UUID) {
); type = IdentifierType.HANDLE;
} const prefix = route.params.idType;
const handleId = route.params.id;
private getLookupParams(route: ActivatedRouteSnapshot): LookupParams { id = `hdl:${prefix}/${handleId}`;
let type;
let id; } else if (route.params.idType === IdentifierType.HANDLE) {
const idType = route.params.idType; type = IdentifierType.HANDLE;
id = 'hdl:' + route.params.id;
// If the idType is not recognized, assume a legacy handle request (handle/prefix/id)
if (idType !== IdentifierType.HANDLE && idType !== IdentifierType.UUID) { } else {
type = IdentifierType.HANDLE; type = IdentifierType.UUID;
const prefix = route.params.idType; id = route.params.id;
const handleId = route.params.id;
id = `hdl:${prefix}/${handleId}`;
} else if (route.params.idType === IdentifierType.HANDLE) {
type = IdentifierType.HANDLE;
id = 'hdl:' + route.params.id;
} else {
type = IdentifierType.UUID;
id = route.params.id;
}
return {
type: type,
id: id,
};
} }
return {
type: type,
id: id,
};
} }

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn,
NavigationExtras, NavigationExtras,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
@@ -18,48 +19,38 @@ import { MYDSPACE_ROUTE } from './my-dspace-page.component';
/** /**
* Prevent unauthorized activating and loading of mydspace configuration * Prevent unauthorized activating and loading of mydspace configuration
* @class MyDSpaceGuard
*/ */
@Injectable({ providedIn: 'root' }) export const MyDSpaceGuard: CanActivateFn = (
export class MyDSpaceGuard { route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
configurationService: MyDSpaceConfigurationService = inject(MyDSpaceConfigurationService),
router: Router = inject(Router),
): Observable<boolean> => {
return configurationService.getAvailableConfigurationTypes().pipe(
first(),
map((configurationList) => validateConfigurationParam(route.queryParamMap.get('configuration'), configurationList)));
};
/** /**
* @constructor * Check if the given configuration is present in the list of those available
*/ *
constructor(private configurationService: MyDSpaceConfigurationService, private router: Router) { * @param configuration
} * the configuration to validate
* @param configurationList
* the list of available configuration
*
*/
function validateConfigurationParam(configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean {
const configurationDefault: string = configurationList[0];
if (isEmpty(configuration) || !configurationList.includes(configuration as MyDSpaceConfigurationValueType)) {
// If configuration param is empty or is not included in available configurations redirect to a default configuration value
const navigationExtras: NavigationExtras = {
queryParams: { configuration: configurationDefault },
};
/** this.router.navigate([MYDSPACE_ROUTE], navigationExtras);
* True when configuration is valid return false;
* @method canActivate } else {
*/ return true;
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.configurationService.getAvailableConfigurationTypes().pipe(
first(),
map((configurationList) => this.validateConfigurationParam(route.queryParamMap.get('configuration'), configurationList)));
}
/**
* Check if the given configuration is present in the list of those available
*
* @param configuration
* the configuration to validate
* @param configurationList
* the list of available configuration
*
*/
private validateConfigurationParam(configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean {
const configurationDefault: string = configurationList[0];
if (isEmpty(configuration) || !configurationList.includes(configuration as MyDSpaceConfigurationValueType)) {
// If configuration param is empty or is not included in available configurations redirect to a default configuration value
const navigationExtras: NavigationExtras = {
queryParams: { configuration: configurationDefault },
};
this.router.navigate([MYDSPACE_ROUTE], navigationExtras);
return false;
} else {
return true;
}
} }
} }

View File

@@ -16,7 +16,7 @@ import {
import { RegistrationGuard } from './registration.guard'; import { RegistrationGuard } from './registration.guard';
describe('RegistrationGuard', () => { describe('RegistrationGuard', () => {
let guard: RegistrationGuard; let guard: any;
let epersonRegistrationService: EpersonRegistrationService; let epersonRegistrationService: EpersonRegistrationService;
let router: Router; let router: Router;
@@ -65,7 +65,7 @@ describe('RegistrationGuard', () => {
setRedirectUrl: {}, setRedirectUrl: {},
}); });
guard = new RegistrationGuard(epersonRegistrationService, router, authService); guard = RegistrationGuard;
}); });
describe('canActivate', () => { describe('canActivate', () => {
@@ -75,21 +75,21 @@ describe('RegistrationGuard', () => {
}); });
it('should return true', (done) => { it('should return true', (done) => {
guard.canActivate(route, state).subscribe((result) => { guard(route, state, authService, epersonRegistrationService, router).subscribe((result) => {
expect(result).toEqual(true); expect(result).toEqual(true);
done(); done();
}); });
}); });
it('should add the response to the route\'s data', (done) => { it('should add the response to the route\'s data', (done) => {
guard.canActivate(route, state).subscribe(() => { guard(route, state, authService, epersonRegistrationService, router).subscribe(() => {
expect(route.data).toEqual({ ...startingRouteData, registration: registrationRD }); expect(route.data).toEqual({ ...startingRouteData, registration: registrationRD });
done(); done();
}); });
}); });
it('should not redirect', (done) => { it('should not redirect', (done) => {
guard.canActivate(route, state).subscribe(() => { guard(route, state, authService, epersonRegistrationService, router).subscribe(() => {
expect(router.navigateByUrl).not.toHaveBeenCalled(); expect(router.navigateByUrl).not.toHaveBeenCalled();
done(); done();
}); });
@@ -102,7 +102,7 @@ describe('RegistrationGuard', () => {
}); });
it('should redirect', () => { it('should redirect', () => {
guard.canActivate(route, state).subscribe(); guard(route, state, authService, epersonRegistrationService, router).subscribe();
expect(router.navigateByUrl).toHaveBeenCalled(); expect(router.navigateByUrl).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -12,37 +13,25 @@ import { EpersonRegistrationService } from '../core/data/eperson-registration.se
import { redirectOn4xx } from '../core/shared/authorized.operators'; import { redirectOn4xx } from '../core/shared/authorized.operators';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
@Injectable({
providedIn: 'root',
})
/** /**
* A guard responsible for redirecting to 4xx pages upon retrieving a Registration object * A guard responsible for redirecting to 4xx pages upon retrieving a Registration object
* The guard also adds the resulting RemoteData<Registration> object to the route's data for further usage in components * The guard also adds the resulting RemoteData<Registration> 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 * The reason this is a guard and not a resolver, is because it has to run before the EndUserAgreementCookieGuard
*/ */
export class RegistrationGuard { export const RegistrationGuard: CanActivateFn = (
constructor(private epersonRegistrationService: EpersonRegistrationService, route: ActivatedRouteSnapshot,
private router: Router, state: RouterStateSnapshot,
private authService: AuthService) { authService: AuthService = inject(AuthService),
} epersonRegistrationService: EpersonRegistrationService = inject(EpersonRegistrationService),
router: Router = inject(Router),
/** ): Observable<boolean> => {
* Can the user activate the route? Returns true if the provided token resolves to an existing Registration, false if const token = route.params.token;
* not. Redirects to 4xx page on 4xx error. Adds the resulting RemoteData<Registration> object to the route's return epersonRegistrationService.searchByToken(token).pipe(
* data.registration property getFirstCompletedRemoteData(),
* @param route redirectOn4xx(router, authService),
* @param state map((rd) => {
*/ route.data = { ...route.data, registration: rd };
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { return rd.hasSucceeded;
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;
}),
);
}
}

View File

@@ -1,25 +1,22 @@
import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable()
/** /**
* Assemble the correct i18n key for the configuration search page's title depending on the current route's configuration parameter. * Assemble the correct i18n key for the configuration search page's title depending on the current route's configuration parameter.
* The format of the key will be "{configuration}.search.title" with: * The format of the key will be "{configuration}.search.title" with:
* - configuration: The current configuration stored in route.params * - configuration: The current configuration stored in route.params
*/ */
export class ConfigurationSearchPageGuard { export const ConfigurationSearchPageGuard: CanActivateFn = (
canActivate( route: ActivatedRouteSnapshot,
route: ActivatedRouteSnapshot, state: RouterStateSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { ): boolean => {
const configuration = route.params.configuration; const configuration = route.params.configuration;
const newTitle = configuration + '.search.title'; const newTitle = `${configuration}.search.title`;
route.data = { title: newTitle }; route.data = { title: newTitle };
return true; return true;
} };
}

View File

@@ -10,6 +10,10 @@ import {
Injectable, Injectable,
TransferState, TransferState,
} from '@angular/core'; } from '@angular/core';
import {
NavigationStart,
Router,
} from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
@@ -30,7 +34,6 @@ import { coreSelector } from '../../app/core/core.selectors';
import { RootDataService } from '../../app/core/data/root-data.service'; import { RootDataService } from '../../app/core/data/root-data.service';
import { LocaleService } from '../../app/core/locale/locale.service'; import { LocaleService } from '../../app/core/locale/locale.service';
import { MetadataService } from '../../app/core/metadata/metadata.service'; import { MetadataService } from '../../app/core/metadata/metadata.service';
import { ServerCheckGuard } from '../../app/core/server-check/server-check.guard';
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
import { InitService } from '../../app/init.service'; import { InitService } from '../../app/init.service';
import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service';
@@ -76,7 +79,7 @@ export class BrowserInitService extends InitService {
protected themeService: ThemeService, protected themeService: ThemeService,
protected menuService: MenuService, protected menuService: MenuService,
private rootDataService: RootDataService, private rootDataService: RootDataService,
protected serverCheckGuard: ServerCheckGuard, protected router: Router,
) { ) {
super( super(
store, store,
@@ -198,7 +201,25 @@ export class BrowserInitService extends InitService {
*/ */
protected initRouteListeners(): void { protected initRouteListeners(): void {
super.initRouteListeners(); super.initRouteListeners();
this.serverCheckGuard.listenForRouteChanges(); this.listenForRouteChanges();
}
/**
* Listen to all router events. Every time a new navigation starts, invalidate the cache
* for the root endpoint. That way we retrieve it once per routing operation to ensure the
* backend is not down. But if the guard is called multiple times during the same routing
* operation, the cached version is used.
*/
protected listenForRouteChanges(): void {
// we'll always be too late for the first NavigationStart event with the router subscribe below,
// so this statement is for the very first route operation.
this.rootDataService.invalidateRootCache();
this.router.events.pipe(
filter(event => event instanceof NavigationStart),
).subscribe(() => {
this.rootDataService.invalidateRootCache();
});
} }
} }