Merge branch 'dmp-refactoring' of https://code-repo.d4science.org/MaDgiK-CITE/argos into dmp-refactoring

This commit is contained in:
Sofia Papacharalampous 2024-05-31 14:23:46 +03:00
commit 56a4ed4da8
21 changed files with 74 additions and 318 deletions

View File

@ -1,33 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from './services/auth/auth.service';
import { AppRole } from './common/enum/app-role';
@Injectable()
export class AdminAuthGuard implements CanActivate, CanLoad {
constructor(private auth: AuthService, private router: Router) {
}
isAdmin(): boolean {
if (!this.auth.currentAccountIsAuthenticated()) { return false; }
return this.auth.hasRole(AppRole.Admin);
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const url: string = state.url;
if (!this.isAdmin()) {
this.router.navigate(['/unauthorized'], { queryParams: { returnUrl: url } });
return false;
}
return true;
}
canLoad(route: Route): boolean {
const url = `/${route.path}`;
if (!this.isAdmin()) {
this.router.navigate(['/unauthorized'], { queryParams: { returnUrl: url } });
return false;
}
return true;
}
}

View File

@ -1,6 +1,5 @@
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { CookieService } from 'ngx-cookie-service'; import { CookieService } from 'ngx-cookie-service';
import { AdminAuthGuard } from './admin-auth-guard.service';
import { AuthGuard } from './auth-guard.service'; import { AuthGuard } from './auth-guard.service';
import { AuthService } from './services/auth/auth.service'; import { AuthService } from './services/auth/auth.service';
import { ContactSupportService } from './services/contact-support/contact-support.service'; import { ContactSupportService } from './services/contact-support/contact-support.service';
@ -20,8 +19,6 @@ import { ProgressIndicationService } from './services/progress-indication/progre
import { TimezoneService } from './services/timezone/timezone-service'; import { TimezoneService } from './services/timezone/timezone-service';
import { CollectionUtils } from './services/utilities/collection-utils.service'; import { CollectionUtils } from './services/utilities/collection-utils.service';
import { TypeUtils } from './services/utilities/type-utils.service'; import { TypeUtils } from './services/utilities/type-utils.service';
import { SpecialAuthGuard } from './special-auth-guard.service';
//import { KeycloakService } from 'keycloak-angular';
import { CanDeactivateGuard } from '@app/library/deactivate/can-deactivate.guard'; import { CanDeactivateGuard } from '@app/library/deactivate/can-deactivate.guard';
import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { FilterService } from '@common/modules/text-filter/filter-service'; import { FilterService } from '@common/modules/text-filter/filter-service';
@ -70,8 +67,6 @@ export class CoreServiceModule {
AuthService, AuthService,
CookieService, CookieService,
BaseHttpV2Service, BaseHttpV2Service,
AdminAuthGuard,
SpecialAuthGuard,
AuthGuard, AuthGuard,
CultureService, CultureService,
TimezoneService, TimezoneService,

View File

@ -279,7 +279,7 @@ export class AuthService extends BaseService {
}) })
.catch((error) => this.onAuthenticateError(error)); .catch((error) => this.onAuthenticateError(error));
} else { } else {
this.zone.run(() => this.router.navigate([returnUrl])); this.zone.run(() => this.router.navigateByUrl(returnUrl));
} }
} }
@ -327,7 +327,7 @@ export class AuthService extends BaseService {
this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-LOGIN'), this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-LOGIN'),
SnackBarNotificationLevel.Success SnackBarNotificationLevel.Success
); );
this.zone.run(() => this.router.navigate([returnUrl])); this.zone.run(() => this.router.navigateByUrl(returnUrl));
} }
onAuthenticateSuccessReload(): void { onAuthenticateSuccessReload(): void {

View File

@ -3,7 +3,6 @@ import { AppAccount } from '@app/core/model/auth/principal';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ConfigurationService } from '../configuration/configuration.service'; import { ConfigurationService } from '../configuration/configuration.service';
import { BaseHttpV2Service } from '../http/base-http-v2.service'; import { BaseHttpV2Service } from '../http/base-http-v2.service';
import { map } from 'rxjs/operators';
import { Tenant } from '@app/core/model/tenant/tenant'; import { Tenant } from '@app/core/model/tenant/tenant';
@Injectable() @Injectable()

View File

@ -1,55 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from './services/auth/auth.service';
import { AppRole } from './common/enum/app-role';
@Injectable()
export class SpecialAuthGuard implements CanActivate, CanLoad {
constructor(private auth: AuthService, private router: Router) {
}
hasPermission(permission: AppRole): boolean {
if (!this.auth.currentAccountIsAuthenticated()) { return false; }
return this.auth.hasRole(permission);
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const url: string = state.url;
const permissions = route.data['authContext']['permissions'];
let count = permissions.length;
if (count < 0 || count === undefined) {
return false;
}
for (let i = 0; i < permissions.length; i++) {
if (!this.hasPermission(permissions[i])) {
count--;
}
}
if (count === 0) {
this.router.navigate(['/unauthorized'], { queryParams: { returnUrl: url } });
return false;
} else {
return true;
}
}
canLoad(route: Route): boolean {
const url = `/${route.path}`;
const permissions = route.data['authContext']['permissions'];
let count = permissions.length;
if (count < 0 || count === undefined) {
return false;
}
for (let i = 0; i < permissions.length; i++) {
if (!this.hasPermission(permissions[i])) {
count--;
}
}
if (count === 0) {
this.router.navigate(['/unauthorized'], { queryParams: { returnUrl: url } });
return false;
} else {
return true;
}
}
}

View File

@ -1,11 +1,11 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { MaintenanceTasksComponent } from './maintenance-tasks.component'; import { MaintenanceTasksComponent } from './maintenance-tasks.component';
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service'; import { AuthGuard } from '@app/core/auth-guard.service';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: MaintenanceTasksComponent, canActivate: [AdminAuthGuard] }, { path: '', component: MaintenanceTasksComponent, canActivate: [AuthGuard] },
]; ];
@NgModule({ @NgModule({

View File

@ -1,6 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service';
import { PrefillingSourceEditorComponent } from './editor/prefilling-source-editor.component'; import { PrefillingSourceEditorComponent } from './editor/prefilling-source-editor.component';
import { PrefillingSourceListingComponent } from './listing/prefilling-source-listing.component'; import { PrefillingSourceListingComponent } from './listing/prefilling-source-listing.component';
import { AppPermission } from '@app/core/common/enum/permission.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum';

View File

@ -1,6 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service';
import { ReferenceTypeEditorComponent } from './editor/reference-type-editor.component'; import { ReferenceTypeEditorComponent } from './editor/reference-type-editor.component';
import { ReferenceTypeListingComponent } from './listing/reference-type-listing.component'; import { ReferenceTypeListingComponent } from './listing/reference-type-listing.component';
import { AuthGuard } from '@app/core/auth-guard.service'; import { AuthGuard } from '@app/core/auth-guard.service';

View File

@ -1,6 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service';
import { ReferenceEditorComponent } from './editor/reference-editor.component'; import { ReferenceEditorComponent } from './editor/reference-editor.component';
import { ReferenceListingComponent } from './listing/reference-listing.component'; import { ReferenceListingComponent } from './listing/reference-listing.component';
import { AppPermission } from '@app/core/common/enum/permission.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum';

View File

@ -1,6 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service';
import { TenantEditorComponent } from './editor/tenant-editor.component'; import { TenantEditorComponent } from './editor/tenant-editor.component';
import { TenantListingComponent } from './listing/tenant-listing.component'; import { TenantListingComponent } from './listing/tenant-listing.component';
import { AppPermission } from '@app/core/common/enum/permission.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum';

View File

@ -1,7 +1,10 @@
import { Component, Input, NgZone, OnInit } from '@angular/core'; import { Component, Input, NgZone, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { AuthService } from '@app/core/services/auth/auth.service'; import { AuthService } from '@app/core/services/auth/auth.service';
import { PrincipalService } from '@app/core/services/http/principal.service';
import { BaseComponent } from '@common/base/base.component'; import { BaseComponent } from '@common/base/base.component';
import { BaseHttpParams } from '@common/http/base-http-params';
import { InterceptorType } from '@common/http/interceptors/interceptor-type';
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from 'keycloak-angular';
import { from } from 'rxjs'; import { from } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -24,6 +27,7 @@ export class LoginComponent extends BaseComponent implements OnInit {
private router: Router, private router: Router,
private authService: AuthService, private authService: AuthService,
private route: ActivatedRoute, private route: ActivatedRoute,
private principalService: PrincipalService,
private keycloakService: KeycloakService private keycloakService: KeycloakService
) { super(); } ) { super(); }
@ -35,13 +39,34 @@ export class LoginComponent extends BaseComponent implements OnInit {
if (!this.authService.selectedTenant()) { if (!this.authService.selectedTenant()) {
this.authService.selectedTenant('default'); this.authService.selectedTenant('default');
} }
const params = new BaseHttpParams();
params.interceptorContext = {
excludedInterceptors: [InterceptorType.TenantHeaderInterceptor]
};
this.principalService.myTenants({ params: params }).subscribe(myTenants => {
if (myTenants) {
if (this.authService.selectedTenant()) {
if (myTenants.findIndex(x => x.code.toLocaleLowerCase() == this.authService.selectedTenant().toLocaleLowerCase()) < 0) {
this.authService.selectedTenant(null);
}
}
if (!this.authService.selectedTenant()) {
if (myTenants.length > 0) {
this.authService.selectedTenant(myTenants[0]?.code);
}
}
} else {
this.authService.selectedTenant(null);
}
this.authService.prepareAuthRequest(from(this.keycloakService.getToken())).pipe(takeUntil(this._destroyed)).subscribe( this.authService.prepareAuthRequest(from(this.keycloakService.getToken())).pipe(takeUntil(this._destroyed)).subscribe(
() => { () => {
let returnUrL = this.returnUrl; let returnUrL = this.returnUrl;
let queryParams: Params = {}; this.zone.run(() => this.router.navigateByUrl(returnUrL));
this.zone.run(() => this.router.navigate([returnUrL], { queryParams }));
}, },
(error) => this.authService.authenticate('/')); (error) => this.authService.authenticate('/'));
}, (error) => this.authService.authenticate('/'));
} }
} }

View File

@ -6,7 +6,6 @@ import { CommonFormsModule } from '@common/forms/common-forms.module';
import { CommonUiModule } from '@common/ui/common-ui.module'; import { CommonUiModule } from '@common/ui/common-ui.module';
import { MergeEmailConfirmation } from './merge-email-confirmation/merge-email-confirmation.component'; import { MergeEmailConfirmation } from './merge-email-confirmation/merge-email-confirmation.component';
import { UnlinkEmailConfirmation } from './unlink-email-confirmation/unlink-email-confirmation.component'; import { UnlinkEmailConfirmation } from './unlink-email-confirmation/unlink-email-confirmation.component';
import { PostLoginComponent } from './post-login/post-login.component';
@NgModule({ @NgModule({
imports: [ imports: [
@ -17,8 +16,7 @@ import { PostLoginComponent } from './post-login/post-login.component';
declarations: [ declarations: [
LoginComponent, LoginComponent,
MergeEmailConfirmation, MergeEmailConfirmation,
UnlinkEmailConfirmation, UnlinkEmailConfirmation
PostLoginComponent
], ],
exports: [ exports: [
LoginComponent LoginComponent

View File

@ -4,7 +4,7 @@ import { LoginComponent } from './login.component';
import { MergeEmailConfirmation } from './merge-email-confirmation/merge-email-confirmation.component'; import { MergeEmailConfirmation } from './merge-email-confirmation/merge-email-confirmation.component';
import { UnlinkEmailConfirmation } from './unlink-email-confirmation/unlink-email-confirmation.component'; import { UnlinkEmailConfirmation } from './unlink-email-confirmation/unlink-email-confirmation.component';
import { AuthGuard } from '@app/core/auth-guard.service'; import { AuthGuard } from '@app/core/auth-guard.service';
import { PostLoginComponent } from './post-login/post-login.component'; // import { PostLoginComponent } from './post-login/post-login.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: LoginComponent }, { path: '', component: LoginComponent },
@ -13,10 +13,10 @@ const routes: Routes = [
component: MergeEmailConfirmation, component: MergeEmailConfirmation,
canActivate: [AuthGuard] canActivate: [AuthGuard]
}, },
{ // {
path: 'post', // path: 'post',
component: PostLoginComponent // component: PostLoginComponent
}, // },
{ path: 'unlink/confirmation/:token', component: UnlinkEmailConfirmation }, { path: 'unlink/confirmation/:token', component: UnlinkEmailConfirmation },
]; ];

View File

@ -1,38 +0,0 @@
<div class="container-fluid">
<div class="tenant-choose-dialog row">
<div class="col-12 col-md-8 offset-md-2 col-lg-4 offset-lg-4">
<mat-card>
<mat-card-header>
<h1 mat-card-title> {{'TENANT-CHOOSE-DIALOG.TITLE' | translate}}</h1>
</mat-card-header>
<mat-card-content>
<form *ngIf="formGroup" class="dialog-form" (ngSubmit)="formSubmit()" [formGroup]="formGroup">
<div class="row">
<mat-form-field appearance="fill" class="col-md-12">
<mat-label>{{'TENANT-CHOOSE-DIALOG.FIELDS.TENANT-CODE' | translate}}</mat-label>
<mat-select formControlName="tenantCode">
<mat-option *ngFor="let tenant of tenants" [value]="tenant.code">
{{tenant.code}}
</mat-option>
</mat-select>
<mat-error *ngIf="formGroup.get('tenantCode').hasError('backendError')">{{formGroup.get('tenantCode').getError('backendError')?.message}}</mat-error>
<mat-error *ngIf="formGroup.get('tenantCode').hasError('required')">{{'COMMONS.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-12">
<div class="row editor-actions">
<div class="col mt-2">
<button type="button" class="default-btn" (click)="cancel()">{{'TENANT-CHOOSE-DIALOG.ACTIONS.BACK' | translate}}</button>
</div>
<div class="col-auto mt-2">
<!-- <button mat-raised-button color="primary" (click)="save($event)" type="primary">{{'TENANT-CHOOSE-DIALOG.ACTIONS.SUBMIT' | translate}}</button> -->
<button type="button" class="normal-btn" (click)="save($event)">{{'TENANT-CHOOSE-DIALOG.ACTIONS.SUBMIT' | translate}}</button>
</div>
</div>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
</div>
</div>

View File

@ -1,17 +0,0 @@
.tenant-choose-dialog {
padding-top: 1em;
.editor-actions {
margin-top: 30px;
}
.confirmation-message {
padding-bottom: 1rem;
font-size: 0.8rem;
}
.display-flex {
display: flex;
font-size: 0.8rem !important;
}
}

View File

@ -1,113 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Tenant } from '@app/core/model/tenant/tenant';
import { AuthService } from '@app/core/services/auth/auth.service';
import { ConfigurationService } from '@app/core/services/configuration/configuration.service';
import { PrincipalService } from '@app/core/services/http/principal.service';
import { BaseComponent } from '@common/base/base.component';
import { FormService } from '@common/forms/form-service';
import { BaseHttpParams } from '@common/http/base-http-params';
import { InterceptorType } from '@common/http/interceptors/interceptor-type';
import { KeycloakService } from 'keycloak-angular';
import { from } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
templateUrl: './post-login.component.html',
styleUrls: ['./post-login.component.scss'],
})
export class PostLoginComponent extends BaseComponent implements OnInit {
formGroup: UntypedFormGroup = null;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
protected tenants: Partial<Tenant>[] = [];
protected selectedTenant = null;
returnUrl: string;
constructor(
private formService: FormService,
private route: ActivatedRoute,
private router: Router,
protected installationConfiguration: ConfigurationService,
private authService: AuthService,
private keycloakService: KeycloakService,
private principalService: PrincipalService
) {
super();
}
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
tenantCode: ['', [Validators.required]]
});
if (this.authService.hasAccessToken()) {
this.loadUserTenants();
} else {
this.keycloakService.getToken().then(token => {
this.authService.currentAuthenticationToken(token);
this.loadUserTenants();
});
}
}
loadUserTenants() {
const params = new BaseHttpParams();
params.interceptorContext = {
excludedInterceptors: [InterceptorType.TenantHeaderInterceptor]
};
if (this.authService.selectedTenant()) {
this.loadUser();
return;
}
this.principalService.myTenants({ params: params }).subscribe(myTenants => {
if (myTenants) {
if (myTenants.length > 1) {
this.tenants = myTenants;
} else if (myTenants.length === 1) {
this.authService.selectedTenant(myTenants[0]?.code);
this.loadUser();
} else {
this.authService.selectedTenant(null);
//this.loadUser();
}
} else {
this.authService.selectedTenant(null);
//this.loadUser();
}
});
}
loadUser(): void {
const returnUrl = '/home';
// const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl') || '/home';
this.authService.prepareAuthRequest(from(this.keycloakService.getToken()), {}).pipe(takeUntil(this._destroyed)).subscribe(() => this.authService.onAuthenticateSuccess(returnUrl), (error) => this.authService.onAuthenticateError(error));
}
save(e: Event): void {
e.preventDefault();
this.formSubmit();
this.loadUser();
}
cancel(): void {
this.router.navigate(['/logout']);
}
formSubmit(): void {
this.formService.touchAllFormFields(this.formGroup);
if (!this.isFormValid()) { return; }
this.selectedTenant = this.formGroup.value['tenantCode'];
this.authService.selectedTenant(this.selectedTenant);
}
public isFormValid(): Boolean {
return this.formGroup.valid;
}
clearErrorModel() {
this.formService.validateAllFormFields(this.formGroup);
}
}

View File

@ -51,7 +51,7 @@ export class LoginService extends BaseService {
if (this.authService.currentAccountIsAuthenticated() && this.authService.getUserProfileCulture()) { this.cultureService.cultureSelected(this.authService.getUserProfileCulture()); } if (this.authService.currentAccountIsAuthenticated() && this.authService.getUserProfileCulture()) { this.cultureService.cultureSelected(this.authService.getUserProfileCulture()); }
if (this.authService.currentAccountIsAuthenticated() && this.authService.getUserProfileLanguage()) { this.language.changeLanguage(this.authService.getUserProfileLanguage()); } if (this.authService.currentAccountIsAuthenticated() && this.authService.getUserProfileLanguage()) { this.language.changeLanguage(this.authService.getUserProfileLanguage()); }
const redirectUrl = returnUrl || '/'; const redirectUrl = returnUrl || '/';
this.router.navigate([redirectUrl]); this.zone.run(() => this.router.navigateByUrl(redirectUrl));
}); });
} }

View File

@ -1,11 +1,11 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { SupportiveMaterialEditorComponent } from './supportive-material-editor.component'; import { SupportiveMaterialEditorComponent } from './supportive-material-editor.component';
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service'; import { AuthGuard } from '@app/core/auth-guard.service';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: SupportiveMaterialEditorComponent, canActivate: [AdminAuthGuard] }, { path: '', component: SupportiveMaterialEditorComponent, canActivate: [AuthGuard] },
]; ];
@NgModule({ @NgModule({

View File

@ -16,14 +16,14 @@ export class StatusCodeInterceptor extends BaseInterceptor {
type: InterceptorType; type: InterceptorType;
interceptRequest(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent <any>> { interceptRequest(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent <any>> {
return next.handle(req).pipe(tap(event => { }, err => { return next.handle(req).pipe(tap(event => { }, err => {
if (err.status === 480) { // if (err.status === 480) {
this.router.navigate(['confirmation']); // this.router.navigate(['confirmation']);
} // }
const error: HttpError = this.httpErrorHandlingService.getError(err); // const error: HttpError = this.httpErrorHandlingService.getError(err);
if (error.statusCode === 403 && error.errorCode === 103) { // if (error.statusCode === 403 && error.errorCode === 103) {
this.authService.selectedTenant('default'); // this.authService.selectedTenant('default');
this.router.navigate(['/']); // this.router.navigate(['/']);
} // }
})); }));
} }

View File

@ -1,6 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service';
import { AppPermission } from '@app/core/common/enum/permission.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum';
import { AuthGuard } from '@app/core/auth-guard.service'; import { AuthGuard } from '@app/core/auth-guard.service';
import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service'; import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service';