feat(authorization): Implemented guards to avoid unauthorized access. (TV-515)

Squashed commit of the following:

commit 86aa3af3f54be4ef5bfb99baece6654a7fba204f
Merge: f3258e8 1e45fb5
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Thu Sep 9 05:42:46 2021 +0200

    Merge branch 'develop' into feature/TV-515-authorization-flow

commit f3258e8c6e3d51f21ec619e09c82b2d0f581bde9
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 8 16:43:44 2021 +0200

    Fixed tests

commit 91bfea1baa297f34769a33972fd61481dfa31197
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 8 15:55:13 2021 +0200

    Removed unused pages

commit d4a92fbde9d6255d8406abc23fe1479658035787
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 8 15:51:25 2021 +0200

    Updated some styling

commit dc75656ff96ff0358a2dd0a8b090b4b4938b8323
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 8 15:35:04 2021 +0200

    Refactured guards by separating organizations into its own guard

commit 24f3a0a2d821930bd682b854f98e1c9816ece08c
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 8 15:33:53 2021 +0200

    Readded search on employees

commit f1890b104c48d6dd6e263b730dbdafbc2a6fbf0f
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 8 14:59:24 2021 +0200

    Added RoleGuard to pages needing a guard

commit ef4b37e3dcc8fe26eef1bb813cfb35727ba691be
Merge: 07bca2a b06436a
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 8 14:06:34 2021 +0200

    Merge branch 'develop' into feature/TV-515-authorization-flow

commit 07bca2a84d0ec970188c284ba4b950312cec57cb
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Wed Sep 8 13:26:50 2021 +0200

    Added check for navigation
This commit is contained in:
Erik Tiekstra
2021-09-09 06:13:17 +02:00
parent 1e45fb5da5
commit c984912a87
49 changed files with 266 additions and 263 deletions

View File

@@ -1,50 +1,35 @@
import { NgModule } from '@angular/core';
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
import { RoleEnum } from '@msfa-enums/role.enum';
import { environment } from '@msfa-environment';
import { AuthGuard } from '@msfa-guards/auth.guard';
import { OrganizationGuard } from '@msfa-guards/organization.guard';
import { RoleGuard } from '@msfa-guards/role.guard';
const routes: Routes = [
{
path: '',
data: { title: '' },
loadChildren: () => import('./pages/start/start.module').then(m => m.StartModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard],
},
{
path: 'administration',
data: { title: 'Administration' },
data: { title: 'Administration', expectedRole: RoleEnum.MSFA_AuthAdmin },
loadChildren: () => import('./pages/administration/administration.module').then(m => m.AdministrationModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
},
{
path: 'deltagare',
data: { title: 'Deltagare' },
data: { title: 'Deltagare', expectedRole: RoleEnum.MSFA_ReportAndPlanning },
loadChildren: () => import('./pages/deltagare/deltagare.module').then(m => m.DeltagareModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
},
{
path: 'nya-deltagare',
data: { title: 'Nya deltagare' },
data: { title: 'Nya deltagare', expectedRole: RoleEnum.MSFA_ReceiveDeltagare },
loadChildren: () => import('./pages/avrop/avrop.module').then(m => m.AvropModule),
canActivate: [AuthGuard],
},
{
path: 'meddelanden',
data: { title: 'Meddelanden' },
loadChildren: () => import('./pages/messages/messages.module').then(m => m.MessagesModule),
canActivate: [AuthGuard],
},
{
path: 'statistik',
data: { title: 'Statistik' },
loadChildren: () => import('./pages/statistics/statistics.module').then(m => m.StatisticsModule),
canActivate: [AuthGuard],
},
{
path: 'installningar',
data: { title: 'Inställningar' },
loadChildren: () => import('./pages/settings/settings.module').then(m => m.SettingsModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard, RoleGuard],
},
{
path: 'logga-ut',
@@ -57,18 +42,19 @@ const routes: Routes = [
data: { title: 'Välj organisation' },
loadChildren: () =>
import('./pages/organization-picker/organization-picker.module').then(m => m.OrganizationPickerModule),
canActivate: [AuthGuard],
},
{
path: 'mitt-konto',
data: { title: 'Mitt konto' },
loadChildren: () => import('./pages/my-account/my-account.module').then(m => m.MyAccountModule),
canActivate: [AuthGuard],
canActivate: [AuthGuard, OrganizationGuard],
},
{
path: 'obehorig',
data: { title: 'Saknar behörighet' },
loadChildren: () => import('./pages/unauthorized/unauthorized.module').then(m => m.UnauthorizedModule),
canActivate: [],
canActivate: [AuthGuard],
},
];

View File

@@ -71,7 +71,7 @@
}
&__edit-button {
@include msfa-button('secondary');
@include msfa__button('secondary');
}
&__authorization-icon {

View File

@@ -140,7 +140,7 @@
(afOnPrimaryClick)="onFormSubmitted(true)"
(afOnSecondaryClick)="abortFormSubmit()"
(afOnInactive)="abortFormSubmit()"
afHeading="Är du säker"
afHeading="Är du säker?"
afHeadingLevel="h2"
afPrimaryButtonText="Ja, spara ändå"
afSecondaryButtonText="Nej, gå tillbaka"

View File

@@ -47,7 +47,7 @@
}
&__link-btn {
@include msfa-button('secondary');
@include msfa__button('secondary');
}
&__choose-all-utforande-verksamheter {

View File

@@ -20,11 +20,11 @@
<h2>Personallista</h2>
<form class="employees__search-wrapper" (ngSubmit)="setSearchFilter()">
<!-- <digi-form-input-search
af-label="Sök personal"
af-label-description="Sök på namn"
<digi-form-input-search
af-label="Sök personalens namn"
(afOnInput)="setSearchValue($event)"
></digi-form-input-search> -->
></digi-form-input-search>
<digi-form-checkbox
class="employees__only-employees-without-authorization"
af-label="Visa endast personal utan behörigheter"

View File

@@ -12,6 +12,7 @@
&__search-wrapper {
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--l;
max-width: var(--digi--typography--text--max-width);
margin-bottom: $digi--layout--gutter--xl;
}

View File

@@ -1,3 +0,0 @@
<msfa-layout>
<section class="messages">Meddelanden funkar!</section>
</msfa-layout>

View File

@@ -1,29 +0,0 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { MessagesComponent } from './messages.component';
describe('MessagesComponent', () => {
let component: MessagesComponent;
let fixture: ComponentFixture<MessagesComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [MessagesComponent, LayoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(MessagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,9 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'msfa-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessagesComponent {}

View File

@@ -1,11 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { MessagesComponent } from './messages.component';
@NgModule({
declarations: [MessagesComponent],
imports: [CommonModule, RouterModule.forChild([{ path: '', component: MessagesComponent }]), LayoutModule],
})
export class MessagesModule {}

View File

@@ -1,9 +1,29 @@
<msfa-layout>
<digi-typography>
<section class="my-account">
<h1>Mitt konto</h1>
<header class="my-account__header">
<h1>Mitt konto</h1>
<a class="my-account__logout" routerLink="/logga-ut">
<msfa-icon [icon]="IconType.LOGOUT"></msfa-icon>
Logga ut
</a>
</header>
<digi-ng-link-internal afText="Logga ut" afRoute="/logga-ut"></digi-ng-link-internal>
<main *ngIf="user$ | async as user; else loadingRef">
<p>Här kan du se dina uppgifter.</p>
<h2>Mina roller</h2>
<ul class="my-account__roles">
<li class="my-account__role" *ngFor="let role of user.roles">
<digi-icon-check-circle class="msfa__digi-icon my-account__authorization-icon"></digi-icon-check-circle>
{{role.name}}
</li>
</ul>
</main>
</section>
</digi-typography>
</msfa-layout>
<ng-template #loadingRef>
<digi-ng-skeleton-base [afCount]="3" afText="Laddar kontoinformation"></digi-ng-skeleton-base>
</ng-template>

View File

@@ -0,0 +1,32 @@
@import 'mixins/buttons';
@import 'mixins/list';
@import 'variables/gutters';
.my-account {
&__header {
display: flex;
justify-content: space-between;
align-items: center;
}
&__logout {
@include msfa__button('secondary');
}
&__roles {
@include msfa__reset-list;
display: flex;
flex-direction: column;
gap: $digi--layout--gutter--s;
}
&__role {
display: flex;
align-items: center;
gap: $digi--layout--gutter--s;
}
&__authorization-icon {
color: var(--digi--ui--color--border--success);
}
}

View File

@@ -1,5 +1,7 @@
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { MyAccountComponent } from './my-account.component';
describe('MyAccountComponent', () => {
@@ -9,8 +11,9 @@ describe('MyAccountComponent', () => {
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [MyAccountComponent],
imports: [HttpClientTestingModule, RouterTestingModule],
}).compileComponents();
})
);

View File

@@ -1,4 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IconType } from '@msfa-enums/icon-type.enum';
import { User } from '@msfa-models/user.model';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
@Component({
selector: 'msfa-my-account',
@@ -6,4 +10,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
styleUrls: ['./my-account.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyAccountComponent {}
export class MyAccountComponent {
user$: Observable<User> = this.userService.user$;
readonly IconType = IconType;
constructor(private userService: UserService) {}
}

View File

@@ -1,7 +1,8 @@
import { DigiNgLinkInternalModule } from '@af/digi-ng/_link/link-internal';
import { DigiNgSkeletonBaseModule } from '@af/digi-ng/_skeleton/skeleton-base';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { IconModule } from '@msfa-shared/components/icon/icon.module';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { MyAccountComponent } from './my-account.component';
@@ -12,7 +13,8 @@ import { MyAccountComponent } from './my-account.component';
CommonModule,
RouterModule.forChild([{ path: '', component: MyAccountComponent }]),
LayoutModule,
DigiNgLinkInternalModule,
IconModule,
DigiNgSkeletonBaseModule,
],
})
export class MyAccountModule {}

View File

@@ -5,7 +5,7 @@ import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { Organization } from '@msfa-models/organization.model';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { filter } from 'rxjs/operators';
@Component({
selector: 'msfa-organization-picker',
@@ -14,9 +14,8 @@ import { filter, map } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OrganizationPickerComponent extends UnsubscribeDirective {
organizations$: Observable<Organization[]> = this.userService.user$.pipe(
filter(user => !!(user && user.organizations?.length)),
map(({ organizations }) => organizations)
organizations$: Observable<Organization[]> = this.userService.organizations$.pipe(
filter(organizations => !!organizations?.length)
);
constructor(

View File

@@ -1,3 +0,0 @@
<msfa-layout>
<section class="settings">Inställningar funkar!</section>
</msfa-layout>

View File

@@ -1,29 +0,0 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [SettingsComponent, LayoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,9 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'msfa-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SettingsComponent {}

View File

@@ -1,11 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { SettingsComponent } from './settings.component';
@NgModule({
declarations: [SettingsComponent],
imports: [CommonModule, RouterModule.forChild([{ path: '', component: SettingsComponent }]), LayoutModule],
})
export class SettingsModule {}

View File

@@ -1,3 +0,0 @@
<msfa-layout>
<section class="statistics">Statistik funkar!</section>
</msfa-layout>

View File

@@ -1,29 +0,0 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LayoutComponent } from '@msfa-shared/components/layout/layout.component';
import { StatisticsComponent } from './statistics.component';
describe('StatisticsComponent', () => {
let component: StatisticsComponent;
let fixture: ComponentFixture<StatisticsComponent>;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
declarations: [StatisticsComponent, LayoutComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(StatisticsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,9 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'msfa-statistics',
templateUrl: './statistics.component.html',
styleUrls: ['./statistics.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StatisticsComponent {}

View File

@@ -1,11 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
import { StatisticsComponent } from './statistics.component';
@NgModule({
declarations: [StatisticsComponent],
imports: [CommonModule, RouterModule.forChild([{ path: '', component: StatisticsComponent }]), LayoutModule],
})
export class StatisticsModule {}

View File

@@ -1,5 +1,5 @@
@import 'mixins/link';
.back-link {
@include msfa-link(true);
@include msfa__link(true);
}

View File

@@ -25,6 +25,26 @@
></path>
</svg>
<svg
*ngIf="icon === iconType.BUILDING"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
width="25"
height="25"
>
<path
d="M436 480h-20V24c0-13.255-10.745-24-24-24H56C42.745 0 32 10.745 32 24v456H12c-6.627 0-12 5.373-12 12v20h448v-20c0-6.627-5.373-12-12-12zM128 76c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12V76zm0 96c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12v-40zm52 148h-40c-6.627 0-12-5.373-12-12v-40c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40c0 6.627-5.373 12-12 12zm76 160h-64v-84c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v84zm64-172c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12v-40c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40zm0-96c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12v-40c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40zm0-96c0 6.627-5.373 12-12 12h-40c-6.627 0-12-5.373-12-12V76c0-6.627 5.373-12 12-12h40c6.627 0 12 5.373 12 12v40z"
fill="currentColor"
></path>
</svg>
<svg *ngIf="icon === iconType.LOGOUT" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="25" height="25">
<path
d="M497 273L329 441c-15 15-41 4.5-41-17v-96H152c-13.3 0-24-10.7-24-24v-96c0-13.3 10.7-24 24-24h136V88c0-21.4 25.9-32 41-17l168 168c9.3 9.4 9.3 24.6 0 34zM192 436v-40c0-6.6-5.4-12-12-12H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h84c6.6 0 12-5.4 12-12V76c0-6.6-5.4-12-12-12H96c-53 0-96 43-96 96v192c0 53 43 96 96 96h84c6.6 0 12-5.4 12-12z"
fill="currentColor"
></path>
</svg>
<svg
*ngIf="icon === iconType.CLIPBOARD"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -2,7 +2,14 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/c
import { IconSize } from '@msfa-enums/icon-size.enum';
import { IconType } from '@msfa-enums/icon-type.enum';
const CUSTOM_ICONS: IconType[] = [IconType.HOME, IconType.SETTINGS, IconType.PLUS, IconType.CLIPBOARD];
const CUSTOM_ICONS: IconType[] = [
IconType.HOME,
IconType.SETTINGS,
IconType.PLUS,
IconType.CLIPBOARD,
IconType.BUILDING,
IconType.LOGOUT,
];
@Component({
selector: 'msfa-icon',

View File

@@ -1,16 +1,20 @@
<div class="navigation">
<div class="navigation__logo-wrapper">
<a [routerLink]="['/']" aria-label="Till startsidan för FA Mina sidor">
<a class="navigation__logo-link" [routerLink]="['/']" aria-label="Till startsidan för FA Mina sidor">
<digi-logo af-system-name="Mina sidor för fristående aktörer" af-color="secondary"></digi-logo>
</a>
</div>
<ul class="navigation__list msfa__hide-on-print">
<li *ngIf="user" class="navigation__item">
<ul class="navigation__list msfa__hide-on-print" *ngIf="user">
<li class="navigation__item">
<a routerLink="/mitt-konto" class="navigation__link">
<msfa-icon [icon]="iconType.USER" size="l"></msfa-icon>
<span class="navigation__text">{{ user.fullName }}</span>
</a>
</li>
<li *ngIf="selectedOrganization" class="navigation__item navigation__item--without-link">
<msfa-icon [icon]="iconType.BUILDING" size="l"></msfa-icon>
<span class="navigation__text">{{ selectedOrganization.name }}</span>
</li>
<!-- <li class="navigation__item">
<a routerLink="/" class="navigation__link">
<msfa-icon [icon]="iconType.BELL" size="l"></msfa-icon>

View File

@@ -24,6 +24,10 @@
align-items: center;
}
&__logo-link {
text-decoration: none;
}
&__logo {
height: $msfa__navigation-height / 2.5;
vertical-align: middle;
@@ -37,16 +41,15 @@
@include msfa__reset-list;
display: flex;
height: 100%;
gap: $digi--layout--gutter--l;
gap: $digi--layout--gutter;
color: var(--digi--typography--color--text--light);
margin-right: var(--digi--layout--gutter);
}
&__item {
display: flex;
}
&__user,
&__item--without-link,
&__link {
display: flex;
align-items: center;

View File

@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { IconType } from '@msfa-enums/icon-type.enum';
import { Organization } from '@msfa-models/organization.model';
import { User } from '@msfa-models/user.model';
@Component({
@@ -10,5 +11,6 @@ import { User } from '@msfa-models/user.model';
})
export class NavigationComponent {
@Input() user: User;
@Input() selectedOrganization: Organization;
iconType = IconType;
}

View File

@@ -11,20 +11,20 @@
Hem
</a>
</li>
<li class="sidebar__item">
<li class="sidebar__item" *ngIf="isReceiveDeltagare">
<a [routerLink]="['/nya-deltagare']" [routerLinkActive]="['sidebar__link--active']" class="sidebar__link">
<msfa-icon class="sidebar__icon" [icon]="iconType.CLIPBOARD" size="xl"></msfa-icon>
Nya deltagare
</a>
</li>
<li class="sidebar__item">
<li class="sidebar__item" *ngIf="isReportAndPlanning">
<a [routerLink]="['/deltagare']" [routerLinkActive]="['sidebar__link--active']" class="sidebar__link">
<msfa-icon class="sidebar__icon" [icon]="iconType.SOK_KANDIDAT" size="xl"></msfa-icon>
Deltagarlista
</a>
</li>
<li class="sidebar__item">
<li class="sidebar__item" *ngIf="isAuthAdmin">
<a [routerLink]="['/administration']" [routerLinkActive]="['sidebar__link--active']" class="sidebar__link">
<msfa-icon class="sidebar__icon" [icon]="iconType.SETTINGS" size="xl"></msfa-icon>
Administration

View File

@@ -1,5 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { IconType } from '@msfa-enums/icon-type.enum';
import { RoleEnum } from '@msfa-enums/role.enum';
import { Role } from '@msfa-models/role.model';
@Component({
selector: 'msfa-sidebar',
@@ -8,5 +10,16 @@ import { IconType } from '@msfa-enums/icon-type.enum';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SidebarComponent {
@Input() userRoles: Role[];
iconType = IconType;
get isAuthAdmin(): boolean {
return this.userRoles?.some(role => role.type === RoleEnum.MSFA_AuthAdmin);
}
get isReceiveDeltagare(): boolean {
return this.userRoles?.some(role => role.type === RoleEnum.MSFA_ReceiveDeltagare);
}
get isReportAndPlanning(): boolean {
return this.userRoles?.some(role => role.type === RoleEnum.MSFA_ReportAndPlanning);
}
}

View File

@@ -2,10 +2,10 @@
<msfa-skip-to-content mainContentId="msfa-main-content"></msfa-skip-to-content>
<header class="msfa__header">
<msfa-navigation [user]="user$ | async"></msfa-navigation>
<msfa-navigation [user]="user$ | async" [selectedOrganization]="selectedOrganization$ | async"></msfa-navigation>
</header>
<msfa-sidebar class="msfa__sidebar"></msfa-sidebar>
<msfa-sidebar class="msfa__sidebar" [userRoles]="roles$ | async"></msfa-sidebar>
<main id="msfa-main-content" class="msfa__content">
<digi-ng-navigation-breadcrumbs
*ngIf="showBreadCrumbs"

View File

@@ -3,12 +3,14 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { Organization } from '@msfa-models/organization.model';
import { Role } from '@msfa-models/role.model';
import { User } from '@msfa-models/user.model';
import { AuthenticationService } from '@msfa-services/api/authentication.service';
import { UserService } from '@msfa-services/api/user.service';
import { mapPathsToBreadcrumbs } from '@msfa-utils/map-paths-to-breadcrumbs.util';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import { filter, map } from 'rxjs/operators';
@Component({
selector: 'msfa-layout',
@@ -18,15 +20,17 @@ import { filter, switchMap } from 'rxjs/operators';
})
export class LayoutComponent extends UnsubscribeDirective {
@Input() showBreadCrumbs = true;
private startBreadcrumb: NavigationBreadcrumbsItem = {
private readonly _startBreadcrumb: NavigationBreadcrumbsItem = {
text: 'Start',
routerLink: '/',
};
private _breadcrumbsItems$ = new BehaviorSubject<NavigationBreadcrumbsItem[]>([this.startBreadcrumb]);
private _breadcrumbsItems$ = new BehaviorSubject<NavigationBreadcrumbsItem[]>([this._startBreadcrumb]);
isLoggedIn$: Observable<boolean> = this.authenticationService.isLoggedIn$;
user$: Observable<User> = this.isLoggedIn$.pipe(
filter(loggedIn => !!loggedIn),
switchMap(() => this.userService.user$)
selectedOrganization$: Observable<Organization> = this.userService.selectedOrganization$;
user$: Observable<User> = this.userService.user$;
roles$: Observable<Role[]> = this.user$.pipe(
filter(user => !!user),
map(user => user.roles)
);
get breadcrumbsItems(): NavigationBreadcrumbsItem[] {
@@ -63,7 +67,7 @@ export class LayoutComponent extends UnsubscribeDirective {
.toString()
.split('/')
.filter(path => !!path);
this._breadcrumbsItems$.next(mapPathsToBreadcrumbs(paths, this.startBreadcrumb));
this._breadcrumbsItems$.next(mapPathsToBreadcrumbs(paths, this._startBreadcrumb));
})
);
}

View File

@@ -3,6 +3,8 @@ export enum IconType {
SETTINGS = 'settings', // Custom
PLUS = 'plus', // Custom
CLIPBOARD = 'clipboard', // Custom
BUILDING = 'building', // Custom
LOGOUT = 'logout', // Custom
USER = 'user',
USERS = 'users',
BELL = 'bell',

View File

@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { environment } from '@msfa-environment';
import { AuthenticationService } from '@msfa-services/api/authentication.service';
import { UserService } from '@msfa-services/api/user.service';
import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
@@ -10,24 +9,13 @@ import { map, switchMap } from 'rxjs/operators';
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(
private authenticationService: AuthenticationService,
private router: Router,
private userService: UserService
) {}
constructor(private authenticationService: AuthenticationService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.authenticationService.isLoggedIn$.pipe(
switchMap(loggedIn => {
if (loggedIn) {
return this.userService.selectedOrganization$.pipe(
map(organization => {
if (!organization) {
void this.router.navigateByUrl(`/organization-picker`);
}
return true;
})
);
return of(true);
} else if (route.queryParams.code) {
return this.authenticationService.login$(route.queryParams.code).pipe(map(result => !!result));
}

View File

@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class OrganizationGuard implements CanActivate {
constructor(private router: Router, private userService: UserService) {}
canActivate(): Observable<boolean> {
return this.userService.selectedOrganization$.pipe(
map(organization => {
if (!organization) {
void this.router.navigateByUrl(`/organization-picker`);
}
return true;
})
);
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { RoleEnum } from '@msfa-enums/role.enum';
import { UserService } from '@msfa-services/api/user.service';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class RoleGuard implements CanActivate {
constructor(private router: Router, private userService: UserService) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const expectedRole: RoleEnum = route.data.expectedRole as RoleEnum;
return this.userService.user$.pipe(
filter(user => !!user),
map(({ roles }) => {
const userHasRole = roles.some(role => role.type === expectedRole);
if (userHasRole) {
return true;
}
void this.router.navigateByUrl('/obehorig');
})
);
}
}

View File

@@ -1,6 +1,8 @@
import { RoleEnum } from '@msfa-enums/role.enum';
export interface UserInfoResponse {
id: string;
firstName: string;
lastName: string;
roles: string[];
roles: RoleEnum[];
}

View File

@@ -1,11 +1,12 @@
import { UserInfoResponse } from './api/user-info.response.model';
import { mapResponseToRoles, Role } from './role.model';
export interface UserInfo {
id: string;
firstName: string;
lastName: string;
fullName: string;
roles: string[];
roles: Role[];
}
export function mapResponseToUserInfo(data: UserInfoResponse): UserInfo {
@@ -16,6 +17,6 @@ export function mapResponseToUserInfo(data: UserInfoResponse): UserInfo {
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
roles,
roles: mapResponseToRoles(roles),
};
}

View File

@@ -1,6 +1,7 @@
import { OrganizationResponse } from './api/organization.response.model';
import { UserInfoResponse } from './api/user-info.response.model';
import { mapResponseToOrganization, Organization } from './organization.model';
import { mapResponseToRoles } from './role.model';
import { UserInfo } from './user-info.model';
export interface User extends UserInfo {
@@ -15,7 +16,7 @@ export function mapUserApiResponseToUser(userInfo: UserInfoResponse, organizatio
firstName,
lastName,
fullName: `${firstName} ${lastName}`,
roles,
roles: mapResponseToRoles(roles),
organizations: organizations ? organizations.map(organization => mapResponseToOrganization(organization)) : [],
};
}

View File

@@ -17,23 +17,34 @@ import { AuthenticationService } from './authentication.service';
})
export class UserService extends UnsubscribeDirective {
private _apiBaseUrl = `${environment.api.url}/auth`;
private _isLoggedIn$: Observable<boolean> = this.authenticationService.isLoggedIn$;
private _organizations$ = new BehaviorSubject<Organization[]>(null);
public organizations$: Observable<Organization[]> = this._organizations$.asObservable();
private _user$ = new BehaviorSubject<User>(null);
public user$: Observable<User> = this._user$.asObservable();
private _selectedOrganizationNumber$ = new BehaviorSubject<string>(null);
constructor(private httpClient: HttpClient, private authenticationService: AuthenticationService) {
super();
this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber);
super.unsubscribeOnDestroy(
this.authenticationService.isLoggedIn$
this._isLoggedIn$
.pipe(
filter(loggedIn => !!loggedIn),
switchMap(() => combineLatest([this._fetchUserInfo$(), this._fetchOrganizations$()]))
switchMap(() => this._fetchOrganizations$())
)
.subscribe(([userInfo, organizations]) => {
this._user$.next({ ...userInfo, organizations });
.subscribe(organizations => {
this._organizations$.next(organizations);
}),
combineLatest([this._isLoggedIn$, this.selectedOrganization$])
.pipe(
filter(([loggedIn, selectedOrganization]) => !!(loggedIn && selectedOrganization)),
switchMap(() => this._fetchUserInfo$())
)
.subscribe(userInfo => {
this._user$.next({ ...userInfo, organizations: this._organizations$.value });
})
);
this._selectedOrganizationNumber$.next(this._selectedOrganizationNumber);
}
private _fetchOrganizations$(): Observable<Organization[]> {
@@ -55,11 +66,11 @@ export class UserService extends UnsubscribeDirective {
}
public get selectedOrganization$(): Observable<Organization | null> {
return combineLatest([this._selectedOrganizationNumber$, this._user$]).pipe(
filter(([, user]) => !!user),
map(([organizationNumber, user]) => {
return combineLatest([this._selectedOrganizationNumber$, this._organizations$]).pipe(
filter(([, organizations]) => !!organizations?.length),
map(([organizationNumber, organizations]) => {
return organizationNumber
? user.organizations.find(organization => organization.organizationNumber === organizationNumber)
? organizations.find(organization => organization.organizationNumber === organizationNumber)
: null;
})
);

View File

@@ -1,4 +1,6 @@
@mixin msfa-button($type: 'primary') {
@import '~@digi/core/dist/collection/components/_button/button/button.css';
@mixin msfa__button($type: 'primary') {
padding: var(--digi-button--padding);
border-radius: var(--digi-button--border-radius);
transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
@@ -6,7 +8,9 @@
font-weight: var(--digi-button--font-weight);
font-size: var(--digi-button--font-size);
width: var(--digi-button--width);
display: var(--digi-button--display);
display: flex;
align-items: center;
gap: 0.3rem;
text-align: var(--digi-button--text-align);
border: var(--digi-button--border);
outline: var(--digi-button--outline);
@@ -31,7 +35,7 @@
@if $type == 'secondary' {
background-color: var(--digi-button--background--secondary--hover);
color: var(--digi-button--color--secondary);
color: var(--digi-button--color--secondary--hover);
} @else if $type == 'tertiary' {
color: var(--digi-button--color--tertiary--hover);
} @else {

View File

@@ -1,4 +1,4 @@
@mixin msfa-link($ignore-visited: false) {
@mixin msfa__link($ignore-visited: false) {
display: inline-flex;
align-items: center;
text-decoration: none;

View File

@@ -83,10 +83,10 @@ dl {
}
&__link {
@include msfa-link(false);
@include msfa__link(false);
&--ignore-visited:visited {
@include msfa-link(true);
@include msfa__link(true);
}
}
}

View File

@@ -1,15 +1,7 @@
@import '~@digi/styles/src/ui/variables/ui__variables';
@import '~@digi/core/dist/collection/components/_button/button/button.css';
// AF DIGI Variables
$digi--ui--color--primary-light: lighten($digi--ui--color--primary, 10%);
$digi--ui--color--primary: $digi--ui--color--stratos;
// Local variables
$msfa-button--background--primary: var(--digi-button--background);
$msfa-button--text--primary: var(--digi--typography--color--text--light);
$msfa-button--hover--primary: var(--digi-button--background--hover);
$msfa-button--background--secondary: var(--digi-button--background--secondary);
$msfa-button--text--secondary: var(--digi--ui--color--primary);
$msfa-button--hover--secondary: var(--digi-button--background--secondary--hover);