{"version":3,"sources":["src/app/core/guards/features.guard.ts","src/app/services/menu/menu.service.ts"],"sourcesContent":["import { inject } from '@angular/core';\nimport { ActivatedRouteSnapshot } from '@angular/router';\nimport { Store } from '@ngxs/store';\nimport {\n FeatureFlags,\n FeaturesState,\n} from 'src/app/store/features/features.state';\n\nexport const RequiredFeatureAnnotation = 'requiredFeature';\n\n// FeatureFlagGuard checks wether a user has the required feature enabled to access a route.\nexport const FeatureFlagGuard = async (snapshot: ActivatedRouteSnapshot) => {\n const store = inject(Store);\n\n if (!snapshot.data || !snapshot.data[RequiredFeatureAnnotation]) return true;\n\n const flags = store.selectSnapshot(FeaturesState.flags) || {};\n\n return HasFlag(flags, snapshot.data[RequiredFeatureAnnotation]);\n};\n\nexport const HasFlag = (flags: FeatureFlags, requiredFeatures: string[]) => {\n // check wether a user has all of the required features enabled\n for (const rp of requiredFeatures) {\n if (flags[rp]) {\n return true;\n }\n }\n\n return false;\n};\n","import { Injectable, OnDestroy, inject } from '@angular/core';\nimport {\n ActivatedRoute,\n ActivatedRouteSnapshot,\n NavigationEnd,\n Router,\n Routes,\n} from '@angular/router';\nimport { Store, createModelSelector, createSelector } from '@ngxs/store';\nimport { Subscription, filter } from 'rxjs';\nimport {\n HasFlag,\n RequiredFeatureAnnotation,\n} from 'src/app/core/guards/features.guard';\nimport { AuthorizedMenuItem } from 'src/app/models/permission';\nimport {\n FeatureFlags,\n FeaturesState,\n FeaturesStateModel,\n} from 'src/app/store/features/features.state';\nimport { UserState, UserStateModel } from 'src/app/store/user/user.state';\nimport {\n HasPermissionForRoute,\n RequiredRolesAnnotation,\n} from '../../core/guards/permission.guard';\nimport { MenuItem } from './menuItem';\n\n@Injectable({\n providedIn: 'root',\n})\nexport class MenuService implements OnDestroy {\n private menuItems: MenuItem[] = [];\n private breadcrumbs: MenuItem[] = [];\n\n private mainLayoutRoutePath = '';\n\n private router = inject(Router);\n private activatedRoute = inject(ActivatedRoute);\n private store = inject(Store);\n\n private menuSubscription: Subscription;\n\n constructor() {\n this.router.events\n .pipe(filter(event => event instanceof NavigationEnd))\n .subscribe(() => {\n this.breadcrumbs = this.createBreadcrumbs(this.activatedRoute.root);\n });\n\n this.menuSubscription = this.store\n .select(\n createModelSelector({\n flags: FeaturesState.flags,\n roles: UserState.getPermissionRoles,\n })\n )\n .subscribe(({ flags, roles }) => {\n if (!flags || !roles) return;\n\n this.generateMenuItems(this.router.config, roles, flags).then(items => {\n this.menuItems = items;\n });\n });\n }\n\n ngOnDestroy(): void {\n this.menuSubscription.unsubscribe();\n }\n\n private async generateMenuItems(\n routes: Routes,\n permissionRoles: string[],\n flags: FeatureFlags\n ): Promise {\n const mainLayoutRoute = routes.find(\n r => r.path === this.mainLayoutRoutePath\n );\n if (!mainLayoutRoute) {\n throw Error(\n `Could not find mainLayoutRoute route with path ${this.mainLayoutRoutePath}. You probably don't want that`\n );\n }\n\n if (!mainLayoutRoute.children) return [];\n\n const items = await Promise.all(\n mainLayoutRoute.children.map(async route => {\n if (!route.data || !route.data['title']) {\n return false;\n }\n\n if (!HasPermissionForRoute(permissionRoles, route)) return false;\n\n if (\n route.data[RequiredFeatureAnnotation] &&\n !HasFlag(flags, route.data[RequiredFeatureAnnotation])\n ) {\n return false;\n }\n\n return {\n label: route.data['title'],\n routerLink: route.path,\n icon: route.data['icon'],\n } as MenuItem;\n })\n );\n\n return items.filter(menuItem => menuItem !== false) as MenuItem[];\n }\n\n updatePageTitle(page: ActivatedRoute, title: string) {\n page.snapshot.data['title'] = title;\n delete page.snapshot.data['dynamicTitle'];\n\n this.breadcrumbs = this.createBreadcrumbs(this.activatedRoute);\n }\n\n private createBreadcrumbs(\n route: ActivatedRoute,\n url = '',\n breadcrumbs: MenuItem[] = []\n ): MenuItem[] {\n const children: ActivatedRoute[] = route.children;\n\n if (children.length === 0) {\n return breadcrumbs;\n }\n\n for (const child of children) {\n // generate router url for child\n const routeURL: string = child.snapshot.url\n .map(segment => segment.path)\n .join('/');\n\n if (routeURL !== '') {\n url += `/${routeURL}`;\n }\n\n // if the route url is empty and there are no children we are on a leaf\n // node and do not need to process further\n if (routeURL === '' && child.children.length === 0) {\n const menuItem = this.getBreadcrumbForSnapshot(child.snapshot, url);\n if (menuItem) {\n breadcrumbs.push(menuItem);\n }\n\n return breadcrumbs;\n }\n\n const menuItem = this.getBreadcrumbForSnapshot(child.snapshot, url);\n if (menuItem) {\n breadcrumbs.push(menuItem);\n }\n\n return this.createBreadcrumbs(child, url, breadcrumbs);\n }\n\n return [];\n }\n\n getBreadcrumbForSnapshot(\n snapshot: ActivatedRouteSnapshot,\n url: string\n ): MenuItem | undefined {\n if (snapshot.data['dynamicTitle']) {\n return {\n label: '',\n routerLink: [url],\n loading: true,\n };\n } else {\n const label = snapshot.data['title'];\n\n if (label) {\n return {\n label: label,\n routerLink: [url],\n };\n }\n }\n\n return;\n }\n\n getMenuItems() {\n return this.menuItems;\n }\n\n getBreadcrumbs() {\n return this.breadcrumbs;\n }\n\n // filter helper functions\n\n static filteredMenuItems(\n allItems: AuthorizedMenuItem[],\n filterSubitems = true\n ) {\n return createSelector(\n [UserState, FeaturesState],\n (state: UserStateModel, features: FeaturesStateModel) => {\n const membership = state.user?.memberships.find(m => {\n return m.organizationId === state.selectedOrganizationId;\n });\n\n if (!membership) return [];\n\n const items = this.filterMenuItems(\n membership.role.permissionRoles,\n features.flags,\n allItems\n );\n\n if (!filterSubitems) return items;\n\n // remove top level items without any sub items which were probably\n // removed by the permission check\n const filteredItems = items.filter(i => {\n if (!i.items || i.items.length === 0) return false;\n return true;\n });\n\n return filteredItems;\n }\n );\n }\n\n // helper function to recursively validate permissions on menu items\n private static filterMenuItems(\n userRoles: string[],\n flags: FeatureFlags,\n items: AuthorizedMenuItem[]\n ): AuthorizedMenuItem[] {\n return items.filter(item => {\n if (item[RequiredRolesAnnotation]) {\n for (const rp of item[RequiredRolesAnnotation]) {\n if (!userRoles.includes(rp)) return false;\n }\n }\n\n if (item[RequiredFeatureAnnotation]) {\n if (!HasFlag(flags, item[RequiredFeatureAnnotation])) return false;\n }\n\n if (item.items) {\n item.items = this.filterMenuItems(userRoles, flags, item.items);\n }\n\n return true;\n });\n }\n}\n"],"names":["RequiredFeatureAnnotation","FeatureFlagGuard","snapshot","__async","store","inject","Store","data","flags","selectSnapshot","FeaturesState","HasFlag","requiredFeatures","rp","MenuService","constructor","menuItems","breadcrumbs","mainLayoutRoutePath","router","inject","Router","activatedRoute","ActivatedRoute","store","Store","events","pipe","filter","event","NavigationEnd","subscribe","createBreadcrumbs","root","menuSubscription","select","createModelSelector","flags","FeaturesState","roles","UserState","getPermissionRoles","generateMenuItems","config","then","items","ngOnDestroy","unsubscribe","routes","permissionRoles","__async","mainLayoutRoute","find","r","path","Error","children","Promise","all","map","route","data","HasPermissionForRoute","RequiredFeatureAnnotation","HasFlag","label","routerLink","icon","menuItem","updatePageTitle","page","title","snapshot","url","length","child","routeURL","segment","join","getBreadcrumbForSnapshot","push","loading","getMenuItems","getBreadcrumbs","filteredMenuItems","allItems","filterSubitems","createSelector","state","features","membership","user","memberships","m","organizationId","selectedOrganizationId","filterMenuItems","role","i","userRoles","item","RequiredRolesAnnotation","rp","includes","factory","ɵfac","providedIn"],"mappings":"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","debug_id":"51fe3fe3-eb9f-5be3-9324-f06e49377f26"}