[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 HOME_PAGE_PATH = 'admin';
export const HOME_PAGE_PATH = 'home';
export function getHomePageRoute() {
return `/${HOME_PAGE_PATH}`;

View File

@@ -11,7 +11,7 @@ import { BrowseByDataType } from './browse-by-switcher/browse-by-data-type';
describe('BrowseByGuard', () => {
describe('canActivate', () => {
let guard: BrowseByGuard;
let guard: any;
let translateService: any;
let browseDefinitionService: any;
let router: any;
@@ -35,7 +35,7 @@ describe('BrowseByGuard', () => {
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', () => {
@@ -53,7 +53,7 @@ describe('BrowseByGuard', () => {
value,
},
};
guard.canActivate(scopedRoute as any, undefined)
guard(scopedRoute as any, undefined, browseDefinitionService, router, translateService)
.pipe(first())
.subscribe(
(canActivate) => {
@@ -86,7 +86,7 @@ describe('BrowseByGuard', () => {
},
};
guard.canActivate(scopedNoValueRoute as any, undefined)
guard(scopedNoValueRoute, undefined, browseDefinitionService, router, translateService)
.pipe(first())
.subscribe(
(canActivate) => {
@@ -123,7 +123,7 @@ describe('BrowseByGuard', () => {
},
};
guard.canActivate(scopedNoValueRoute as any, undefined).pipe(
guard(scopedNoValueRoute as any, undefined, browseDefinitionService, router, translateService).pipe(
first(),
).subscribe((canActivate) => {
const result = {
@@ -154,7 +154,8 @@ describe('BrowseByGuard', () => {
value,
},
};
guard.canActivate(route as any, undefined)
guard(route as any, undefined, browseDefinitionService, router, translateService)
.pipe(first())
.subscribe(
(canActivate) => {
@@ -189,7 +190,8 @@ describe('BrowseByGuard', () => {
value,
},
};
guard.canActivate(scopedRoute as any, undefined)
guard(scopedRoute as any, undefined, browseDefinitionService, router, translateService)
.pipe(first())
.subscribe((canActivate) => {
expect(router.navigate).toHaveBeenCalled();

View File

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

View File

@@ -10,7 +10,7 @@ import { CreateCollectionPageGuard } from './create-collection-page.guard';
describe('CreateCollectionPageGuard', () => {
describe('canActivate', () => {
let guard: CreateCollectionPageGuard;
let guard: any;
let router;
let communityDataServiceStub: any;
@@ -28,11 +28,11 @@ describe('CreateCollectionPageGuard', () => {
};
router = new RouterMock();
guard = new CreateCollectionPageGuard(router, communityDataServiceStub);
guard = CreateCollectionPageGuard;
});
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())
.subscribe(
(canActivate) =>
@@ -41,7 +41,7 @@ describe('CreateCollectionPageGuard', () => {
});
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())
.subscribe(
(canActivate) =>
@@ -50,7 +50,7 @@ describe('CreateCollectionPageGuard', () => {
});
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())
.subscribe(
(canActivate) =>
@@ -59,7 +59,7 @@ describe('CreateCollectionPageGuard', () => {
});
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())
.subscribe(
(canActivate) =>

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
CanActivateFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
@@ -24,34 +24,29 @@ import {
} from '../../shared/empty.util';
/**
* Prevent creation of a collection without a parent community provided
* @class CreateCollectionPageGuard
*/
@Injectable({ providedIn: 'root' })
export class CreateCollectionPageGuard implements CanActivate {
public constructor(private router: Router, private communityService: CommunityDataService) {
}
/**
* 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> {
export const CreateCollectionPageGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
communityService: CommunityDataService = inject(CommunityDataService),
router: Router = inject(Router),
): Observable<boolean> => {
const parentID = route.queryParams.parent;
if (hasNoValue(parentID)) {
this.router.navigate(['/404']);
router.navigate(['/404']);
return observableOf(false);
}
return this.communityService.findById(parentID)
return 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']);
router.navigate(['/404']);
}
}),
);
}
}
};

View File

@@ -10,7 +10,7 @@ import { CreateCommunityPageGuard } from './create-community-page.guard';
describe('CreateCommunityPageGuard', () => {
describe('canActivate', () => {
let guard: CreateCommunityPageGuard;
let guard: any;
let router;
let communityDataServiceStub: any;
@@ -28,11 +28,11 @@ describe('CreateCommunityPageGuard', () => {
};
router = new RouterMock();
guard = new CreateCommunityPageGuard(router, communityDataServiceStub);
guard = CreateCommunityPageGuard;
});
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())
.subscribe(
(canActivate) =>
@@ -41,7 +41,7 @@ describe('CreateCommunityPageGuard', () => {
});
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())
.subscribe(
(canActivate) =>
@@ -50,7 +50,7 @@ describe('CreateCommunityPageGuard', () => {
});
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())
.subscribe(
(canActivate) =>
@@ -59,7 +59,7 @@ describe('CreateCommunityPageGuard', () => {
});
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())
.subscribe(
(canActivate) =>

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
@@ -23,35 +24,29 @@ import {
} from '../../shared/empty.util';
/**
* Prevent creation of a community with an invalid parent community provided
* @class CreateCommunityPageGuard
*/
@Injectable({ providedIn: 'root' })
export class CreateCommunityPageGuard {
public constructor(private router: Router, private communityService: CommunityDataService) {
}
/**
* True when either NO parent ID query parameter has been provided, or 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> {
export const CreateCommunityPageGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
communityService: CommunityDataService = inject(CommunityDataService),
router: Router = inject(Router),
): Observable<boolean> => {
const parentID = route.queryParams.parent;
if (hasNoValue(parentID)) {
return observableOf(true);
}
return this.communityService.findById(parentID)
return 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']);
router.navigate(['/404']);
}
},
),
);
}
}
};

View File

@@ -20,7 +20,7 @@ import { authReducer } from './auth.reducer';
import { AuthBlockingGuard } from './auth-blocking.guard';
describe('AuthBlockingGuard', () => {
let guard: AuthBlockingGuard;
let guard: any;
let initialState;
let store: Store<AppState>;
let mockStore: MockStore<AppState>;
@@ -52,14 +52,14 @@ describe('AuthBlockingGuard', () => {
beforeEach(() => {
store = TestBed.inject(Store);
mockStore = store as MockStore<AppState>;
guard = new AuthBlockingGuard(store);
guard = AuthBlockingGuard;
});
describe(`canActivate`, () => {
describe(`when authState.blocking is undefined`, () => {
it(`should not emit anything`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('-'));
expect(guard(null, null, store)).toBeObservable(cold('-'));
done();
});
});
@@ -77,7 +77,7 @@ describe('AuthBlockingGuard', () => {
});
it(`should not emit anything`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('-'));
expect(guard(null, null, store)).toBeObservable(cold('-'));
done();
});
});
@@ -95,7 +95,7 @@ describe('AuthBlockingGuard', () => {
});
it(`should succeed`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('(a|)', { a: true }));
expect(guard(null, null, store)).toBeObservable(cold('(a|)', { a: true }));
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 {
select,
Store,
@@ -19,24 +24,16 @@ import { isAuthenticationBlocking } from './selectors';
* route until the authentication status has loaded.
* To ensure all rest requests get the correct auth header.
*/
@Injectable({
providedIn: 'root',
})
export class AuthBlockingGuard {
constructor(private store: Store<AppState>) {
}
/**
* True when the authentication isn't blocking everything
*/
canActivate(): Observable<boolean> {
return this.store.pipe(select(isAuthenticationBlocking)).pipe(
export const AuthBlockingGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
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),
);
}
};
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
@@ -10,16 +11,13 @@ import { AuthorizationDataService } from '../data/feature-authorization/authoriz
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 class FeedbackGuard {
export const FeedbackGuard: CanActivateFn = (
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';
describe('ReloadGuard', () => {
let guard: ReloadGuard;
let guard: any;
let router: Router;
let appConfig: AppConfig;
beforeEach(() => {
router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']);
appConfig = new DefaultAppConfig();
guard = new ReloadGuard(router, appConfig);
guard = ReloadGuard;
});
describe('canActivate', () => {
@@ -31,7 +31,7 @@ describe('ReloadGuard', () => {
});
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));
});
});
@@ -44,7 +44,7 @@ describe('ReloadGuard', () => {
});
it('should create a UrlTree to home', () => {
guard.canActivate(route, undefined);
guard(route, undefined, appConfig, router);
expect(router.createUrlTree).toHaveBeenCalledWith(['home']);
});
});

View File

@@ -1,9 +1,7 @@
import {
Inject,
Injectable,
} from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
UrlTree,
@@ -13,33 +11,25 @@ import {
APP_CONFIG,
AppConfig,
} from '../../../config/app-config.interface';
import { HOME_PAGE_PATH } from '../../app-routing-paths';
import { isNotEmpty } from '../../shared/empty.util';
/**
* 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
*/
@Injectable({ providedIn: 'root' })
export class ReloadGuard {
constructor(
private router: Router,
@Inject(APP_CONFIG) private appConfig: AppConfig,
) {
}
/**
* Get the UrlTree of the URL to redirect to
* @param route
* @param state
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree {
export const ReloadGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
appConfig: AppConfig = inject(APP_CONFIG),
router: Router = inject(Router),
): 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)
const url = route.queryParams.redirect.startsWith(appConfig.ui.nameSpace)
? route.queryParams.redirect.substring(appConfig.ui.nameSpace.length)
: route.queryParams.redirect;
return this.router.parseUrl(url);
return router.parseUrl(url);
} else {
return this.router.createUrlTree(['home']);
return router.createUrlTree([HOME_PAGE_PATH]);
}
}
}
};

View File

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

View File

@@ -1,14 +1,13 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
NavigationStart,
CanActivateChildFn,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';
import {
filter,
map,
take,
} from 'rxjs/operators';
@@ -16,52 +15,18 @@ import {
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
import { RootDataService } from '../data/root-data.service';
@Injectable({
providedIn: 'root',
})
/**
* A guard that checks if root api endpoint is reachable.
* If not redirect to 500 error page
*/
export class ServerCheckGuard {
constructor(private router: Router, private rootDataService: RootDataService) {
}
/**
* True when root api endpoint is reachable.
*/
canActivateChild(
export const ServerCheckGuard: CanActivateChildFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean | UrlTree> {
return this.rootDataService.checkServerAvailability().pipe(
rootDataService: RootDataService = inject(RootDataService),
router: Router = inject(Router),
): Observable<boolean | UrlTree> => {
return rootDataService.checkServerAvailability().pipe(
take(1),
map((isAvailable: boolean) => {
if (!isAvailable) {
return this.router.parseUrl(getPageInternalServerErrorRoute());
} else {
return true;
}
}),
map((isAvailable: boolean) => isAvailable ? true : router.parseUrl(getPageInternalServerErrorRoute())),
);
}
/**
* 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,
hasSucceeded: true })),
};
guard = new LookupGuard(dsoService);
guard = LookupGuard;
});
it('should call findByIdAndIDType with handle params', () => {
@@ -22,7 +22,7 @@ describe('LookupGuard', () => {
idType: '123456789',
},
};
guard.canActivate(scopedRoute as any, undefined);
guard(scopedRoute as any, undefined, dsoService);
expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('hdl:123456789/1234', IdentifierType.HANDLE);
});
@@ -33,7 +33,7 @@ describe('LookupGuard', () => {
idType: 'handle',
},
};
guard.canActivate(scopedRoute as any, undefined);
guard(scopedRoute as any, undefined, dsoService);
expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('hdl:123456789%2F1234', IdentifierType.HANDLE);
});
@@ -44,7 +44,7 @@ describe('LookupGuard', () => {
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);
});

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
@@ -16,22 +17,18 @@ interface LookupParams {
id: string;
}
@Injectable({
providedIn: 'root',
})
export class LookupGuard {
constructor(private dsoService: DsoRedirectService) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const params = this.getLookupParams(route);
return this.dsoService.findByIdAndIDType(params.id, params.type).pipe(
export const LookupGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
dsoService: DsoRedirectService = inject(DsoRedirectService),
): Observable<boolean> => {
const params = getLookupParams(route);
return dsoService.findByIdAndIDType(params.id, params.type).pipe(
map((response: RemoteData<DSpaceObject>) => response.hasFailed),
);
}
};
private getLookupParams(route: ActivatedRouteSnapshot): LookupParams {
function getLookupParams(route: ActivatedRouteSnapshot): LookupParams {
let type;
let id;
const idType = route.params.idType;
@@ -55,5 +52,4 @@ export class LookupGuard {
type: type,
id: id,
};
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
NavigationExtras,
Router,
RouterStateSnapshot,
@@ -18,28 +19,19 @@ import { MYDSPACE_ROUTE } from './my-dspace-page.component';
/**
* Prevent unauthorized activating and loading of mydspace configuration
* @class MyDSpaceGuard
*/
@Injectable({ providedIn: 'root' })
export class MyDSpaceGuard {
/**
* @constructor
*/
constructor(private configurationService: MyDSpaceConfigurationService, private router: Router) {
}
/**
* True when configuration is valid
* @method canActivate
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.configurationService.getAvailableConfigurationTypes().pipe(
export const MyDSpaceGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
configurationService: MyDSpaceConfigurationService = inject(MyDSpaceConfigurationService),
router: Router = inject(Router),
): Observable<boolean> => {
return configurationService.getAvailableConfigurationTypes().pipe(
first(),
map((configurationList) => this.validateConfigurationParam(route.queryParamMap.get('configuration'), configurationList)));
}
map((configurationList) => validateConfigurationParam(route.queryParamMap.get('configuration'), configurationList)));
};
/**
/**
* Check if the given configuration is present in the list of those available
*
* @param configuration
@@ -48,7 +40,7 @@ export class MyDSpaceGuard {
* the list of available configuration
*
*/
private validateConfigurationParam(configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean {
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
@@ -61,5 +53,4 @@ export class MyDSpaceGuard {
} else {
return true;
}
}
}

View File

@@ -16,7 +16,7 @@ import {
import { RegistrationGuard } from './registration.guard';
describe('RegistrationGuard', () => {
let guard: RegistrationGuard;
let guard: any;
let epersonRegistrationService: EpersonRegistrationService;
let router: Router;
@@ -65,7 +65,7 @@ describe('RegistrationGuard', () => {
setRedirectUrl: {},
});
guard = new RegistrationGuard(epersonRegistrationService, router, authService);
guard = RegistrationGuard;
});
describe('canActivate', () => {
@@ -75,21 +75,21 @@ describe('RegistrationGuard', () => {
});
it('should return true', (done) => {
guard.canActivate(route, state).subscribe((result) => {
guard(route, state, authService, epersonRegistrationService, router).subscribe((result) => {
expect(result).toEqual(true);
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 });
done();
});
});
it('should not redirect', (done) => {
guard.canActivate(route, state).subscribe(() => {
guard(route, state, authService, epersonRegistrationService, router).subscribe(() => {
expect(router.navigateByUrl).not.toHaveBeenCalled();
done();
});
@@ -102,7 +102,7 @@ describe('RegistrationGuard', () => {
});
it('should redirect', () => {
guard.canActivate(route, state).subscribe();
guard(route, state, authService, epersonRegistrationService, router).subscribe();
expect(router.navigateByUrl).toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from '@angular/router';
@@ -12,37 +13,25 @@ import { EpersonRegistrationService } from '../core/data/eperson-registration.se
import { redirectOn4xx } from '../core/shared/authorized.operators';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
@Injectable({
providedIn: 'root',
})
/**
* 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 reason this is a guard and not a resolver, is because it has to run before the EndUserAgreementCookieGuard
*/
export class RegistrationGuard {
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<Registration> object to the route's
* data.registration property
* @param route
* @param state
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
export const RegistrationGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
authService: AuthService = inject(AuthService),
epersonRegistrationService: EpersonRegistrationService = inject(EpersonRegistrationService),
router: Router = inject(Router),
): Observable<boolean> => {
const token = route.params.token;
return this.epersonRegistrationService.searchByToken(token).pipe(
return epersonRegistrationService.searchByToken(token).pipe(
getFirstCompletedRemoteData(),
redirectOn4xx(this.router, this.authService),
redirectOn4xx(router, authService),
map((rd) => {
route.data = { ...route.data, registration: rd };
return rd.hasSucceeded;
}),
);
}
}
};

View File

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

View File

@@ -10,6 +10,10 @@ import {
Injectable,
TransferState,
} from '@angular/core';
import {
NavigationStart,
Router,
} from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import {
@@ -30,7 +34,6 @@ import { coreSelector } from '../../app/core/core.selectors';
import { RootDataService } from '../../app/core/data/root-data.service';
import { LocaleService } from '../../app/core/locale/locale.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 { InitService } from '../../app/init.service';
import { KlaroService } from '../../app/shared/cookies/klaro.service';
@@ -76,7 +79,7 @@ export class BrowserInitService extends InitService {
protected themeService: ThemeService,
protected menuService: MenuService,
private rootDataService: RootDataService,
protected serverCheckGuard: ServerCheckGuard,
protected router: Router,
) {
super(
store,
@@ -198,7 +201,25 @@ export class BrowserInitService extends InitService {
*/
protected initRouteListeners(): void {
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();
});
}
}