diff --git a/package.json b/package.json index 403973ef98..278afdf6c3 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "clean:json": "rimraf *.records.json", "clean:node": "rimraf node_modules", "clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json", - "clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:dev:config", + "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node", "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts", "build:mirador": "webpack --config webpack/webpack.mirador.config.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts index 948d7d86bc..65026c1504 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts @@ -18,6 +18,8 @@ import { ActivatedRoute } from '@angular/router'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import createSpy = jasmine.createSpy; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { Item } from '../../core/shared/item.model'; describe('AdminSidebarComponent', () => { let comp: AdminSidebarComponent; @@ -26,6 +28,28 @@ describe('AdminSidebarComponent', () => { let authorizationService: AuthorizationDataService; let scriptService; + + const mockItem = Object.assign(new Item(), { + id: 'fake-id', + uuid: 'fake-id', + handle: 'fake/handle', + lastModified: '2018', + _links: { + self: { + href: 'https://localhost:8000/items/fake-id' + } + } + }); + + + const routeStub = { + data: observableOf({ + dso: createSuccessfulRemoteDataObject(mockItem) + }), + children: [] + }; + + beforeEach(waitForAsync(() => { authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: observableOf(true) @@ -42,6 +66,7 @@ describe('AdminSidebarComponent', () => { { provide: ActivatedRoute, useValue: {} }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ScriptDataService, useValue: scriptService }, + { provide: ActivatedRoute, useValue: routeStub }, { provide: NgbModal, useValue: { open: () => {/*comment*/ @@ -229,19 +254,19 @@ describe('AdminSidebarComponent', () => { it('should contain site admin section', () => { expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'admin_search', visible: true, + id: 'admin_search', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'registries', visible: true, + id: 'registries', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - parentID: 'registries', visible: true, + parentID: 'registries', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'curation_tasks', visible: true, + id: 'curation_tasks', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'workflow', visible: true, + id: 'workflow', visible: true, })); }); }); @@ -259,7 +284,7 @@ describe('AdminSidebarComponent', () => { it('should show edit_community', () => { expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'edit_community', visible: true, + id: 'edit_community', visible: true, })); }); }); @@ -277,7 +302,7 @@ describe('AdminSidebarComponent', () => { it('should show edit_collection', () => { expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'edit_collection', visible: true, + id: 'edit_collection', visible: true, })); }); }); @@ -295,10 +320,10 @@ describe('AdminSidebarComponent', () => { it('should show access control section', () => { expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'access_control', visible: true, + id: 'access_control', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - parentID: 'access_control', visible: true, + parentID: 'access_control', visible: true, })); }); }); diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.ts index f0d583744c..c81b2e6e93 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.ts @@ -21,6 +21,7 @@ import { MenuService } from '../../shared/menu/menu.service'; import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { Router, ActivatedRoute } from '@angular/router'; /** * Component representing the admin sidebar @@ -63,14 +64,15 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { inFocus$: BehaviorSubject; constructor(protected menuService: MenuService, - protected injector: Injector, - private variableService: CSSVariableService, - private authService: AuthService, - private modalService: NgbModal, - private authorizationService: AuthorizationDataService, - private scriptDataService: ScriptDataService, + protected injector: Injector, + private variableService: CSSVariableService, + private authService: AuthService, + private modalService: NgbModal, + public authorizationService: AuthorizationDataService, + private scriptDataService: ScriptDataService, + public route: ActivatedRoute ) { - super(menuService, injector); + super(menuService, injector, authorizationService, route); this.inFocus$ = new BehaviorSubject(false); } @@ -144,7 +146,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { type: MenuItemType.TEXT, text: 'menu.section.new' } as TextMenuItemModel, - icon: 'plus', + icon: 'plus', index: 0 }, { diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index 366e1da7b1..62d072c249 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -103,20 +103,20 @@ export class CollectionPageComponent implements OnInit { const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig); this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe( - switchMap(([currentPagination, currentSort ]) => this.collectionRD$.pipe( + switchMap(([currentPagination, currentSort]) => this.collectionRD$.pipe( getFirstSucceededRemoteData(), map((rd) => rd.payload.id), switchMap((id: string) => { return this.searchService.search( - new PaginatedSearchOptions({ - scope: id, - pagination: currentPagination, - sort: currentSort, - dsoTypes: [DSpaceObjectType.ITEM] - })).pipe(toDSpaceObjectListRD()) as Observable>>; + new PaginatedSearchOptions({ + scope: id, + pagination: currentPagination, + sort: currentSort, + dsoTypes: [DSpaceObjectType.ITEM] + })).pipe(toDSpaceObjectListRD()) as Observable>>; }), startWith(undefined) // Make sure switching pages shows loading component - ) + ) ) ); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts new file mode 100644 index 0000000000..680495686e --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { AuthorizationDataService } from '../authorization-data.service'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../../../auth/auth.service'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../feature-id'; + +/** + * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group + * management rights + */ +@Injectable({ + providedIn: 'root' +}) +export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard { + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { + super(authorizationService, router, authService); + } + + /** + * Check group management rights + */ + getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanViewUsageStatistics); + } +} diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index b64de5100b..01a482dc79 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -26,4 +26,5 @@ export enum FeatureID { CanEditVersion = 'canEditVersion', CanDeleteVersion = 'canDeleteVersion', CanCreateVersion = 'canCreateVersion', + CanViewUsageStatistics = 'canViewUsageStatistics', } diff --git a/src/app/navbar/navbar-section/navbar-section.component.html b/src/app/navbar/navbar-section/navbar-section.component.html index b5f6848050..d77b57515e 100644 --- a/src/app/navbar/navbar-section/navbar-section.component.html +++ b/src/app/navbar/navbar-section/navbar-section.component.html @@ -1,4 +1,3 @@ + + \ No newline at end of file diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index 3fa7598e74..fc5d1a2ef3 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -1,18 +1,15 @@ - \ No newline at end of file diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts index 5aa2bf1786..659107080c 100644 --- a/src/app/navbar/navbar.component.spec.ts +++ b/src/app/navbar/navbar.component.spec.ts @@ -14,14 +14,41 @@ import { MenuServiceStub } from '../shared/testing/menu-service.stub'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { BrowseService } from '../core/browse/browse.service'; -import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { buildPaginatedList } from '../core/data/paginated-list.model'; import { BrowseDefinition } from '../core/shared/browse-definition.model'; import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator'; +import { Item } from '../core/shared/item.model'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; let comp: NavbarComponent; let fixture: ComponentFixture; +const authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true) +}); + +const mockItem = Object.assign(new Item(), { + id: 'fake-id', + uuid: 'fake-id', + handle: 'fake/handle', + lastModified: '2018', + _links: { + self: { + href: 'https://localhost:8000/items/fake-id' + } + } +}); + +const routeStub = { + data: observableOf({ + dso: createSuccessfulRemoteDataObject(mockItem) + }), + children: [] +}; + + + describe('NavbarComponent', () => { const menuService = new MenuServiceStub(); let browseDefinitions; @@ -66,8 +93,9 @@ describe('NavbarComponent', () => { Injector, { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - { provide: ActivatedRoute, useValue: {} }, - { provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } } + { provide: ActivatedRoute, useValue: routeStub }, + { provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } }, + { provide: AuthorizationDataService, useValue: authorizationService }, ], schemas: [NO_ERRORS_SCHEMA] }) @@ -76,7 +104,6 @@ describe('NavbarComponent', () => { // synchronous beforeEach beforeEach(() => { - spyOn(menuService, 'getMenuTopSections').and.returnValue(observableOf([])); fixture = TestBed.createComponent(NavbarComponent); @@ -87,4 +114,6 @@ describe('NavbarComponent', () => { it('should create', () => { expect(comp).toBeTruthy(); }); + + }); diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index c3b34d12ee..836f94781a 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -11,6 +11,8 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { PaginatedList } from '../core/data/paginated-list.model'; import { BrowseDefinition } from '../core/shared/browse-definition.model'; import { RemoteData } from '../core/data/remote-data'; +import { ActivatedRoute } from '@angular/router'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; /** * Component representing the public navbar @@ -29,11 +31,13 @@ export class NavbarComponent extends MenuComponent { menuID = MenuID.PUBLIC; constructor(protected menuService: MenuService, - protected injector: Injector, + protected injector: Injector, public windowService: HostWindowService, - public browseService: BrowseService + public browseService: BrowseService, + public authorizationService: AuthorizationDataService, + public route: ActivatedRoute ) { - super(menuService, injector); + super(menuService, injector, authorizationService, route); } ngOnInit(): void { diff --git a/src/app/shared/menu/menu.component.spec.ts b/src/app/shared/menu/menu.component.spec.ts index 883969137b..b84fad2b33 100644 --- a/src/app/shared/menu/menu.component.spec.ts +++ b/src/app/shared/menu/menu.component.spec.ts @@ -7,9 +7,12 @@ import { MenuComponent } from './menu.component'; import { MenuServiceStub } from '../testing/menu-service.stub'; import { of as observableOf } from 'rxjs'; import { MenuSection } from './menu.reducer'; -import { Router } from '@angular/router'; +import { Router, ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { MenuID } from './initial-menus-state'; +import { Item } from '../../core/shared/item.model'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; describe('MenuComponent', () => { let comp: MenuComponent; @@ -19,13 +22,44 @@ describe('MenuComponent', () => { const mockMenuID = 'mock-menuID' as MenuID; + const mockStatisticSection = { 'id': 'statistics_site', 'active': true, 'visible': true, 'index': 2, 'type': 'statistics', 'model': { 'type': 1, 'text': 'menu.section.statistics', 'link': 'statistics' } }; + + let authorizationService: AuthorizationDataService; + + const mockItem = Object.assign(new Item(), { + id: 'fake-id', + uuid: 'fake-id', + handle: 'fake/handle', + lastModified: '2018', + _links: { + self: { + href: 'https://localhost:8000/items/fake-id' + } + } + }); + + + const routeStub = { + data: observableOf({ + dso: createSuccessfulRemoteDataObject(mockItem) + }), + children: [] + }; + beforeEach(waitForAsync(() => { + + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(false) + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule], declarations: [MenuComponent], providers: [ Injector, - { provide: MenuService, useClass: MenuServiceStub } + { provide: MenuService, useClass: MenuServiceStub }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: ActivatedRoute, useValue: routeStub }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(MenuComponent, { @@ -95,4 +129,35 @@ describe('MenuComponent', () => { expect(menuService.collapseMenuPreview).toHaveBeenCalledWith(comp.menuID); })); }); + + describe('when unauthorized statistics', () => { + + beforeEach(() => { + (authorizationService as any).isAuthorized.and.returnValue(observableOf(false)); + fixture.detectChanges(); + }); + + it('should return observable of empty object', done => { + comp.getAuthorizedStatistics(mockStatisticSection).subscribe((res) => { + expect(res).toEqual({}); + done(); + }); + }); + }); + + describe('get authorized statistics', () => { + + beforeEach(() => { + (authorizationService as any).isAuthorized.and.returnValue(observableOf(true)); + fixture.detectChanges(); + }); + + it('should return observable of statistics section menu', done => { + comp.getAuthorizedStatistics(mockStatisticSection).subscribe((res) => { + expect(res).toEqual(mockStatisticSection); + done(); + }); + }); + }); + }); diff --git a/src/app/shared/menu/menu.component.ts b/src/app/shared/menu/menu.component.ts index 32fd938f4e..caf613a33f 100644 --- a/src/app/shared/menu/menu.component.ts +++ b/src/app/shared/menu/menu.component.ts @@ -1,14 +1,17 @@ import { ChangeDetectionStrategy, Component, Injector, OnDestroy, OnInit } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; import { MenuService } from './menu.service'; import { MenuID } from './initial-menus-state'; import { MenuSection } from './menu.reducer'; -import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; +import { distinctUntilChanged, map, mergeMap, switchMap } from 'rxjs/operators'; import { GenericConstructor } from '../../core/shared/generic-constructor'; -import { hasValue } from '../empty.util'; +import { hasValue, isNotEmptyOperator } from '../empty.util'; import { MenuSectionComponent } from './menu-section/menu-section.component'; import { getComponentForMenu } from './menu-section.decorator'; import { compareArraysUsingIds } from '../../item-page/simple/item-types/shared/item-relationships-utils'; +import { ActivatedRoute } from '@angular/router'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; /** * A basic implementation of a MenuComponent @@ -67,27 +70,38 @@ export class MenuComponent implements OnInit, OnDestroy { */ subs: Subscription[] = []; - constructor(protected menuService: MenuService, protected injector: Injector) { + private activatedRouteLastChild: ActivatedRoute; + + constructor(protected menuService: MenuService, protected injector: Injector, public authorizationService: AuthorizationDataService, public route: ActivatedRoute) { } /** * Sets all instance variables to their initial values */ ngOnInit(): void { + this.activatedRouteLastChild = this.getActivatedRoute(this.route); this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID); this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID); this.menuVisible = this.menuService.isMenuVisible(this.menuID); this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds())); + this.subs.push( this.sections.pipe( // if you return an array from a switchMap it will emit each element as a separate event. // So this switchMap is equivalent to a subscribe with a forEach inside switchMap((sections: MenuSection[]) => sections), + mergeMap((section: MenuSection) => { + if (section.id.includes('statistics')) { + return this.getAuthorizedStatistics(section); + } + return observableOf(section); + }), + isNotEmptyOperator(), switchMap((section: MenuSection) => this.getSectionComponent(section).pipe( - map((component: GenericConstructor) => ({ section, component })) + map((component: GenericConstructor) => ({ section, component })) )), - distinctUntilChanged((x,y) => x.section.id === y.section.id) - ).subscribe(({ section, component}) => { + distinctUntilChanged((x, y) => x.section.id === y.section.id) + ).subscribe(({ section, component }) => { const nextMap = this.sectionMap$.getValue(); nextMap.set(section.id, { injector: this.getSectionDataInjector(section), @@ -98,6 +112,43 @@ export class MenuComponent implements OnInit, OnDestroy { ); } + /** + * Get activated route of the deepest activated route + */ + getActivatedRoute(route) { + if (route.children.length > 0) { + return this.getActivatedRoute(route.firstChild); + } else { + return route; + } + } + + /** + * Get section of statistics after checking authorization + */ + getAuthorizedStatistics(section) { + return this.activatedRouteLastChild.data.pipe( + switchMap((data) => { + return this.authorizationService.isAuthorized(FeatureID.CanViewUsageStatistics, this.getObjectUrl(data)).pipe( + map((canViewUsageStatistics: boolean) => { + if (!canViewUsageStatistics) { + return {}; + } else { + return section; + } + })); + }) + ); + } + + /** + * Get statistics route dso data + */ + getObjectUrl(data) { + const object = data.site ? data.site : data.dso.payload; + return object._links.self.href; + } + /** * Collapse this menu when it's currently expanded, expand it when its currently collapsed * @param {Event} event The user event that triggered this method @@ -164,8 +215,8 @@ export class MenuComponent implements OnInit, OnDestroy { private getSectionComponent(section: MenuSection): Observable> { return this.menuService.hasSubSections(this.menuID, section.id).pipe( map((expandable: boolean) => { - return getComponentForMenu(this.menuID, expandable); - } + return getComponentForMenu(this.menuID, expandable); + } ), ); } diff --git a/src/app/shared/menu/menu.effects.ts b/src/app/shared/menu/menu.effects.ts index 47cff90209..5eafad7a98 100644 --- a/src/app/shared/menu/menu.effects.ts +++ b/src/app/shared/menu/menu.effects.ts @@ -19,7 +19,7 @@ export class MenuEffects { /** * On route change, build menu sections for every menu type depending on the current route data */ - @Effect({dispatch: false}) + @Effect({ dispatch: false }) public buildRouteMenuSections$: Observable = this.actions$ .pipe( ofType(ROUTER_NAVIGATED), @@ -31,8 +31,8 @@ export class MenuEffects { ); constructor(private actions$: Actions, - private menuService: MenuService, - private route: ActivatedRoute) { + private menuService: MenuService, + private route: ActivatedRoute) { } /** @@ -72,7 +72,6 @@ export class MenuEffects { const last: boolean = hasNoValue(route.firstChild); if (hasValue(data) && hasValue(data.menu) && hasValue(data.menu[menuID])) { - let menuSections: MenuSection[] | MenuSection = data.menu[menuID]; menuSections = this.resolveSubstitutions(menuSections, params); @@ -120,4 +119,5 @@ export class MenuEffects { } return resolved; } + } diff --git a/src/app/statistics-page/statistics-page-routing.module.ts b/src/app/statistics-page/statistics-page-routing.module.ts index 3c88e096e7..ef6f68d557 100644 --- a/src/app/statistics-page/statistics-page-routing.module.ts +++ b/src/app/statistics-page/statistics-page-routing.module.ts @@ -10,64 +10,69 @@ import { ThemedCommunityStatisticsPageComponent } from './community-statistics-p import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component'; import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component'; import { ItemResolver } from '../item-page/item.resolver'; +import { StatisticsAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard'; @NgModule({ imports: [ StatisticsPageModule, RouterModule.forChild([ - { - path: '', - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { - title: 'statistics.title', - breadcrumbKey: 'statistics' - }, - children: [ - { - path: '', - component: ThemedSiteStatisticsPageComponent, - }, - ] + { + path: '', + resolve: { + breadcrumb: I18nBreadcrumbResolver }, - { - path: `items/:id`, - resolve: { - scope: ItemResolver, - breadcrumb: I18nBreadcrumbResolver - }, - data: { - title: 'statistics.title', - breadcrumbKey: 'statistics' - }, - component: ThemedItemStatisticsPageComponent, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics' }, - { - path: `collections/:id`, - resolve: { - scope: CollectionPageResolver, - breadcrumb: I18nBreadcrumbResolver + children: [ + { + path: '', + component: ThemedSiteStatisticsPageComponent, }, - data: { - title: 'statistics.title', - breadcrumbKey: 'statistics' - }, - component: ThemedCollectionStatisticsPageComponent, + ], + canActivate: [StatisticsAdministratorGuard] + }, + { + path: `items/:id`, + resolve: { + scope: ItemResolver, + breadcrumb: I18nBreadcrumbResolver }, - { - path: `communities/:id`, - resolve: { - scope: CommunityPageResolver, - breadcrumb: I18nBreadcrumbResolver - }, - data: { - title: 'statistics.title', - breadcrumbKey: 'statistics' - }, - component: ThemedCommunityStatisticsPageComponent, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics' }, - ] + component: ThemedItemStatisticsPageComponent, + canActivate: [StatisticsAdministratorGuard] + }, + { + path: `collections/:id`, + resolve: { + scope: CollectionPageResolver, + breadcrumb: I18nBreadcrumbResolver + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics' + }, + component: ThemedCollectionStatisticsPageComponent, + canActivate: [StatisticsAdministratorGuard] + }, + { + path: `communities/:id`, + resolve: { + scope: CommunityPageResolver, + breadcrumb: I18nBreadcrumbResolver + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics' + }, + component: ThemedCommunityStatisticsPageComponent, + canActivate: [StatisticsAdministratorGuard] + }, + ] ) ], providers: [ diff --git a/src/themes/dspace/app/navbar/navbar.component.html b/src/themes/dspace/app/navbar/navbar.component.html index 5af30db632..f061c7cb3b 100644 --- a/src/themes/dspace/app/navbar/navbar.component.html +++ b/src/themes/dspace/app/navbar/navbar.component.html @@ -1,17 +1,14 @@ - \ No newline at end of file