feat(login): Added login page within application with links to CIAM login-functionality. (TV-595)
Merge in TEA/mina-sidor-fa-web from feature/TV-595-ciam-login-page to develop Squashed commit of the following: commit 7796cbc958bfb14dccb6cfc329fb223b66643af1 Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Wed Nov 17 09:46:47 2021 +0100 Using guard to check if user is logged in commit 43b9fca3d0d640b5c9711ec9837222ac2df5c782 Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Wed Nov 17 08:32:10 2021 +0100 Added link-button as logout link to my account commit ab40fae0d4741ee30af146a41ce254c6c7f6658a Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Wed Nov 17 08:25:04 2021 +0100 Refactored authentication a bit commit d1c75864f2a0b1867b372655e81e37b28a067503 Merge: 45f350888f05343eAuthor: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Wed Nov 17 07:04:32 2021 +0100 Merge branch 'develop' into feature/TV-595-ciam-login-page commit 45f3508811de2842af1c095ff72949b619d5bc8d Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Tue Nov 16 16:28:44 2021 +0100 Added resolve to check if user already is logged in when navigating to login page commit 44b212fb1e0eab7fdb823a8f41ea0d780c920ee0 Merge: 56ed0e5754ac27efAuthor: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Tue Nov 16 13:58:58 2021 +0100 Merge branch 'develop' into feature/TV-595-ciam-login-page commit 56ed0e57fb3f19c4c41ec3fe676db41ed5831557 Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Tue Nov 16 13:48:53 2021 +0100 Implemented custom login page commit 27a514758d73d685e80a37e490646a759783d1f5 Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se> Date: Tue Nov 16 11:51:57 2021 +0100 WIP
This commit is contained in:
@@ -6,6 +6,7 @@ import { environment } from '@msfa-environment';
|
|||||||
import { AuthGuard } from '@msfa-guards/auth.guard';
|
import { AuthGuard } from '@msfa-guards/auth.guard';
|
||||||
import { OrganizationGuard } from '@msfa-guards/organization.guard';
|
import { OrganizationGuard } from '@msfa-guards/organization.guard';
|
||||||
import { RoleGuard } from '@msfa-guards/role.guard';
|
import { RoleGuard } from '@msfa-guards/role.guard';
|
||||||
|
import { SkipIfLoggedInGuard } from '@msfa-guards/skip-if-logged-in.guard';
|
||||||
|
|
||||||
const activeFeatures: Feature[] = environment.activeFeatures;
|
const activeFeatures: Feature[] = environment.activeFeatures;
|
||||||
|
|
||||||
@@ -16,6 +17,12 @@ const routes: Routes = [
|
|||||||
loadChildren: () => import('./pages/start/start.module').then(m => m.StartModule),
|
loadChildren: () => import('./pages/start/start.module').then(m => m.StartModule),
|
||||||
canActivate: [AuthGuard, OrganizationGuard],
|
canActivate: [AuthGuard, OrganizationGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'logga-in',
|
||||||
|
data: { title: 'Logga in' },
|
||||||
|
loadChildren: () => import('./pages/login/login.module').then(m => m.LoginModule),
|
||||||
|
canActivate: [SkipIfLoggedInGuard],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'logga-ut',
|
path: 'logga-ut',
|
||||||
data: { title: 'Logga ut' },
|
data: { title: 'Logga ut' },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
|||||||
import { APPLICATION_CLOSED_TIME_STAMP } from '@msfa-constants/local-storage-keys';
|
import { APPLICATION_CLOSED_TIME_STAMP } from '@msfa-constants/local-storage-keys';
|
||||||
import { environment } from '@msfa-environment';
|
import { environment } from '@msfa-environment';
|
||||||
import { AuthenticationService } from '@msfa-services/api/authentication.service';
|
import { AuthenticationService } from '@msfa-services/api/authentication.service';
|
||||||
import { IdleService } from '@msfa-services/api/idle.service';
|
import { IdleService } from '@msfa-services/idle.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, switchMap } from 'rxjs/operators';
|
import { filter, map, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
|||||||
48
apps/mina-sidor-fa/src/app/pages/login/login.component.html
Normal file
48
apps/mina-sidor-fa/src/app/pages/login/login.component.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<msfa-layout>
|
||||||
|
<digi-typography>
|
||||||
|
<section class="login">
|
||||||
|
<header class="login__header">
|
||||||
|
<h1>Hur vill du logga in?</h1>
|
||||||
|
</header>
|
||||||
|
<main class="login__content">
|
||||||
|
<ul class="login__cta-wrapper">
|
||||||
|
<li>
|
||||||
|
<ui-link-button class="login__button" uiSize="l" [uiFullWidth]="true" [uiHref]="mobileBankIdLink">
|
||||||
|
<msfa-icon icon="bankid" size="l"></msfa-icon>
|
||||||
|
Mobilt bank-id
|
||||||
|
</ui-link-button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ui-link-button class="login__button" uiSize="l" [uiFullWidth]="true" [uiHref]="bankIdLink">
|
||||||
|
<msfa-icon icon="bankid" size="l"></msfa-icon>
|
||||||
|
Bank-id
|
||||||
|
</ui-link-button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ui-link-button
|
||||||
|
class="login__button"
|
||||||
|
uiSize="l"
|
||||||
|
[uiFullWidth]="true"
|
||||||
|
uiType="secondary"
|
||||||
|
[uiHref]="passwordLink"
|
||||||
|
>
|
||||||
|
<msfa-icon icon="user"></msfa-icon>
|
||||||
|
Användarnamn och lösenord
|
||||||
|
</ui-link-button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<aside class="login__help">
|
||||||
|
<h2 class="login__sub-heading">Har du problem att logga in?</h2>
|
||||||
|
<ul class="login__links">
|
||||||
|
<li>
|
||||||
|
<digi-link-external af-href="//arbetsformedlingen.se/om-webbplatsen/anvanda-webbplatsen/om-e-legitimation"
|
||||||
|
>Så här fungerar e-legitimation</digi-link-external
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>Vid inloggning med e-legitimation använder vi Visma Consulting AB som leverantör av säker inloggning.</p>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
</digi-typography>
|
||||||
|
</msfa-layout>
|
||||||
36
apps/mina-sidor-fa/src/app/pages/login/login.component.scss
Normal file
36
apps/mina-sidor-fa/src/app/pages/login/login.component.scss
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
@import 'mixins/list';
|
||||||
|
@import 'variables/gutters';
|
||||||
|
|
||||||
|
.login {
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $digi--layout--gutter--xxl;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sub-heading {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cta-wrapper {
|
||||||
|
@include msfa__reset-list;
|
||||||
|
min-width: 24rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $digi--layout--gutter;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help {
|
||||||
|
max-width: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $digi--layout--gutter;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__links {
|
||||||
|
@include msfa__reset-list;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $digi--layout--gutter--s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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 { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(
|
||||||
|
waitForAsync(() => {
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
declarations: [LoginComponent],
|
||||||
|
imports: [RouterTestingModule, HttpClientTestingModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
}).compileComponents();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
apps/mina-sidor-fa/src/app/pages/login/login.component.ts
Normal file
15
apps/mina-sidor-fa/src/app/pages/login/login.component.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { environment } from '@msfa-environment';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'msfa-login',
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrls: ['./login.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class LoginComponent {
|
||||||
|
private readonly _baseLoginUrl = `${environment.ciam.loginUrl}&client_id=${environment.ciam.clientId}&redirect_uri=${window.location.origin}&template=msfa`;
|
||||||
|
mobileBankIdLink = `${this._baseLoginUrl}&acr_values=bankid-mobile`;
|
||||||
|
bankIdLink = `${this._baseLoginUrl}&acr_values=bankid`;
|
||||||
|
passwordLink = `${this._baseLoginUrl}&acr_values=password`;
|
||||||
|
}
|
||||||
20
apps/mina-sidor-fa/src/app/pages/login/login.module.ts
Normal file
20
apps/mina-sidor-fa/src/app/pages/login/login.module.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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 { UiLinkButtonModule } from '@ui/link-button/link-button.module';
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
declarations: [LoginComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterModule.forChild([{ path: '', component: LoginComponent }]),
|
||||||
|
LayoutModule,
|
||||||
|
UiLinkButtonModule,
|
||||||
|
IconModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class LoginModule {}
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
<header class="my-account__header">
|
<header class="my-account__header">
|
||||||
<div class="my-account__heading-wrapper">
|
<div class="my-account__heading-wrapper">
|
||||||
<h1>Mitt konto</h1>
|
<h1>Mitt konto</h1>
|
||||||
<a class="my-account__logout" routerLink="/logga-ut">
|
<ui-link-button uiRouterLink="/logga-ut" uiType="secondary">
|
||||||
<msfa-icon [icon]="IconType.LOGOUT"></msfa-icon>
|
<msfa-icon [icon]="IconType.LOGOUT"></msfa-icon>
|
||||||
Logga ut
|
Logga ut
|
||||||
</a>
|
</ui-link-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="my-account__contents" *ngIf="user$ | async as user; else loadingRef">
|
<main class="my-account__contents" *ngIf="user$ | async as user; else loadingRef">
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
@@ -7,6 +6,8 @@ import { IconModule } from '@msfa-shared/components/icon/icon.module';
|
|||||||
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
|
import { LayoutModule } from '@msfa-shared/components/layout/layout.module';
|
||||||
import { OrganizationPickerFormModule } from '@msfa-shared/components/organization-picker-form/organization-picker-form.module';
|
import { OrganizationPickerFormModule } from '@msfa-shared/components/organization-picker-form/organization-picker-form.module';
|
||||||
import { RolesDialogModule } from '@msfa-shared/components/roles-dialog/roles-dialog.module';
|
import { RolesDialogModule } from '@msfa-shared/components/roles-dialog/roles-dialog.module';
|
||||||
|
import { UiLinkButtonModule } from '@ui/link-button/link-button.module';
|
||||||
|
import { UiSkeletonModule } from '@ui/skeleton/skeleton.module';
|
||||||
import { MyAccountComponent } from './my-account.component';
|
import { MyAccountComponent } from './my-account.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -21,6 +22,7 @@ import { MyAccountComponent } from './my-account.component';
|
|||||||
OrganizationPickerFormModule,
|
OrganizationPickerFormModule,
|
||||||
RolesDialogModule,
|
RolesDialogModule,
|
||||||
HideTextModule,
|
HideTextModule,
|
||||||
|
UiLinkButtonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MyAccountModule {}
|
export class MyAccountModule {}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
<ui-icon-custom *ngIf="isCustomIcon; else digiIcon" aria-hidden="true" class="icon" [ngClass]="[iconClass]">
|
<ui-icon-custom *ngIf="isCustomIcon; else digiIcon" aria-hidden="true" class="icon" [ngClass]="[iconClass]">
|
||||||
|
<svg *ngIf="icon === iconType.BANKID" xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 169 158">
|
||||||
|
<path
|
||||||
|
d="M45.916 135.23c5.268 0 9.905 2.083 9.015 7.63l-1.005 6.257c-.355 2.211-.289 2.873 2.253 2.922l-2.583 5.878c-4.495.291-6.67-.167-7.75-2.046-2.384 1.422-5.006 2.129-7.814 2.129-5.093 0-6.845-2.541-6.392-5.378.213-1.34 1.03-2.67 2.328-3.756 2.794-2.336 9.715-2.673 12.42-4.462.236-2.002-.6-2.712-3.15-2.712-2.979 0-5.466.961-9.714 3.754l1.035-6.47c3.698-2.58 7.251-3.747 11.357-3.747zm32.032 0c4.892 0 7.172 3.008 6.407 7.968L82.075 158h-8.773l1.889-12.256c.342-2.244-.3-3.266-2.028-3.266-1.392 0-2.649.764-3.872 2.414L67.27 158H58.5l3.446-22.395h8.774l-.452 2.929c2.772-2.376 4.896-3.305 7.68-3.305zm22.058-7.436l-2.19 14.881 8.377-8.065H117l-10.759 9.971L114.866 158h-10.973l-6.656-10.808h-.082L95.567 158H86.82l4.438-30.206h8.747zm-78.437.93c7.265 0 9.048 3.578 8.525 6.855-.423 2.664-2.276 4.617-5.514 5.906 4.08 1.496 5.669 3.863 5.084 7.52-.733 4.616-4.87 8.066-10.257 8.066H0l4.514-28.347h17.055zm131.362 0c11.337 0 14.611 7.98 13.563 14.547-1.03 6.442-6.288 13.8-16.25 13.8h-16.53l4.532-28.347h14.685zm-19.681 0l-4.553 28.347h-10.304l4.554-28.347h10.303zm-87.452 19.348c-2.392 1.462-6.8 1.21-7.281 4.213-.228 1.42.685 2.46 2.151 2.46 1.427 0 3.164-.583 4.517-1.5-.091-.498-.047-1.042.113-2.043l.5-3.13zm-30.297-3.262h-3.332l-1.265 7.938h3.076c3.421 0 5.43-1.33 5.872-4.115.38-2.37-1.017-3.823-4.351-3.823zm134.677-9.357h-2.75l-2.379 14.884h2.707c4.978 0 7.721-2.37 8.535-7.439.595-3.741-.573-7.445-6.113-7.445zm-133.187-2.41h-2.95l-1.185 7.44h2.95c3.334 0 4.889-1.704 5.212-3.74.344-2.158-.692-3.7-4.027-3.7zM43.87 14.405c3.239 0 5.74.667 7.04 1.88.907.849 1.245 1.96.977 3.219-.26 1.215-1.64 2.797-3.71 4.223-5.866 4.066-4.85 8.28-4.397 9.477 1.358 3.601 5.892 5.544 9.462 5.544h7.58l-7.07 43.956h.121c-2.632 16.426-4.59 28.813-4.953 31.148H6.964c.804-5.165 11.305-70.804 11.97-75.091h.645l2.179.005h.502l1.696.004.446.001.62.001h.371l.172.001.451.001h.504l.058.001h.076c5.043-.027 9.69-2.3 11.844-5.793 2.106-3.388 1.305-7.02-2.076-9.482-1.127-.818-2.424-2.145-2.17-3.787.348-2.217 4.171-5.308 9.617-5.308zM109.119 0c38.623 0 65.555 16.776 58.858 58.635-5.415 33.793-32.973 55.218-66.709 55.218H93.27l5.425-34.104c13.664-.152 25.113-6.51 27.478-21.265 2.54-15.881-5.526-22.699-20.537-22.815.003-.024.003-.037.003-.037h-11.04c-2.542 0-5.808-1.395-6.655-3.641-.264-.696-.802-3.206 3.314-6.068 1.58-1.101 4.324-3.347 4.9-6.034.471-2.252-.198-4.429-1.845-5.967-1.876-1.757-5.01-2.69-9.061-2.69-6.765 0-11.939 4.042-12.527 7.813-.037.245-.061.532-.061.846 0 1.5.59 3.709 3.397 5.763 1.03.753 2.028 1.86 2.028 3.341 0 .679-.21 1.43-.723 2.267-1.608 2.612-5.341 4.378-9.301 4.4l-7.243-.017L66.526 0z"
|
||||||
|
transform="translate(-100 -239) translate(100 239)"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
<svg *ngIf="icon === iconType.HOME" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="25" height="25">
|
<svg *ngIf="icon === iconType.HOME" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="25" height="25">
|
||||||
<path
|
<path
|
||||||
d="M280.37 148.26L96 300.11V464a16 16 0 0 0 16 16l112.06-.29a16 16 0 0 0 15.92-16V368a16 16 0 0 1 16-16h64a16 16 0 0 1 16 16v95.64a16 16 0 0 0 16 16.05L464 480a16 16 0 0 0 16-16V300L295.67 148.26a12.19 12.19 0 0 0-15.3 0zM571.6 251.47L488 182.56V44.05a12 12 0 0 0-12-12h-56a12 12 0 0 0-12 12v72.61L318.47 43a48 48 0 0 0-61 0L4.34 251.47a12 12 0 0 0-1.6 16.9l25.5 31A12 12 0 0 0 45.15 301l235.22-193.74a12.19 12.19 0 0 1 15.3 0L530.9 301a12 12 0 0 0 16.9-1.6l25.5-31a12 12 0 0 0-1.7-16.93z"
|
d="M280.37 148.26L96 300.11V464a16 16 0 0 0 16 16l112.06-.29a16 16 0 0 0 15.92-16V368a16 16 0 0 1 16-16h64a16 16 0 0 1 16 16v95.64a16 16 0 0 0 16 16.05L464 480a16 16 0 0 0 16-16V300L295.67 148.26a12.19 12.19 0 0 0-15.3 0zM571.6 251.47L488 182.56V44.05a12 12 0 0 0-12-12h-56a12 12 0 0 0-12 12v72.61L318.47 43a48 48 0 0 0-61 0L4.34 251.47a12 12 0 0 0-1.6 16.9l25.5 31A12 12 0 0 0 45.15 301l235.22-193.74a12.19 12.19 0 0 1 15.3 0L530.9 301a12 12 0 0 0 16.9-1.6l25.5-31a12 12 0 0 0-1.7-16.93z"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const CUSTOM_ICONS: IconType[] = [
|
|||||||
IconType.CLIPBOARD,
|
IconType.CLIPBOARD,
|
||||||
IconType.BUILDING,
|
IconType.BUILDING,
|
||||||
IconType.LOGOUT,
|
IconType.LOGOUT,
|
||||||
|
IconType.BANKID,
|
||||||
];
|
];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="msfa" *ngIf="isLoggedIn$ | async">
|
<div class="msfa">
|
||||||
<msfa-skip-to-content mainContentId="msfa-main-content"></msfa-skip-to-content>
|
<msfa-skip-to-content mainContentId="msfa-main-content"></msfa-skip-to-content>
|
||||||
|
|
||||||
<header class="msfa__header">
|
<header class="msfa__header">
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { filter } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class LayoutComponent extends UnsubscribeDirective {
|
export class LayoutComponent extends UnsubscribeDirective {
|
||||||
@Input() showBreadCrumbs = true;
|
@Input() showBreadCrumbs = true;
|
||||||
isLoggedIn$: Observable<boolean> = this.authenticationService.isLoggedIn$;
|
|
||||||
selectedOrganization$: Observable<Organization> = this.userService.selectedOrganization$;
|
selectedOrganization$: Observable<Organization> = this.userService.selectedOrganization$;
|
||||||
user$: Observable<Employee> = this.userService.user$.pipe(filter(user => !!user));
|
user$: Observable<Employee> = this.userService.user$.pipe(filter(user => !!user));
|
||||||
roles$: Observable<Role[]> = this.userService.userRoles$.pipe(filter(roles => !!roles));
|
roles$: Observable<Role[]> = this.userService.userRoles$.pipe(filter(roles => !!roles));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum IconType {
|
|||||||
CLIPBOARD = 'clipboard', // Custom
|
CLIPBOARD = 'clipboard', // Custom
|
||||||
BUILDING = 'building', // Custom
|
BUILDING = 'building', // Custom
|
||||||
LOGOUT = 'logout', // Custom
|
LOGOUT = 'logout', // Custom
|
||||||
|
BANKID = 'bankid', // Custom
|
||||||
USER = 'user',
|
USER = 'user',
|
||||||
USERS = 'users',
|
USERS = 'users',
|
||||||
BELL = 'bell',
|
BELL = 'bell',
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
|
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
|
||||||
import { environment } from '@msfa-environment';
|
|
||||||
import { AuthenticationService } from '@msfa-services/api/authentication.service';
|
import { AuthenticationService } from '@msfa-services/api/authentication.service';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -12,38 +11,27 @@ export class AuthGuard implements CanActivate {
|
|||||||
constructor(private authenticationService: AuthenticationService, private router: Router) {}
|
constructor(private authenticationService: AuthenticationService, private router: Router) {}
|
||||||
|
|
||||||
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
|
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
|
||||||
return this.authenticationService.isLoggedIn$.pipe(
|
const isLoggedIn = this.authenticationService.isLoggedIn;
|
||||||
switchMap(loggedIn => {
|
const isTokenValid = this.authenticationService.isTokenValid;
|
||||||
if (loggedIn) {
|
const isTokenRefreshable = this.authenticationService.isTokenRefreshable;
|
||||||
return this.authenticationService.isTokenValid$.pipe(
|
|
||||||
switchMap(({ isValid, isRefreshable }) => {
|
if (isLoggedIn) {
|
||||||
if (isValid) {
|
if (isTokenValid) {
|
||||||
return of(true);
|
return of(true);
|
||||||
}
|
}
|
||||||
if (isRefreshable) {
|
if (isTokenRefreshable) {
|
||||||
return this.authenticationService.refreshToken$();
|
return this.authenticationService.refreshToken$();
|
||||||
}
|
}
|
||||||
this.redirectToLoginPage();
|
|
||||||
return of(false);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (route.queryParams.code) {
|
} else if (route.queryParams.code) {
|
||||||
return this.authenticationService.login$(route.queryParams.code).pipe(map(result => !!result));
|
return this.authenticationService.login$(route.queryParams.code).pipe(map(result => !!result));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.redirectToLoginPage();
|
this.redirectToLoginPage();
|
||||||
return of(false);
|
return of(false);
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectToLoginPage(): void {
|
redirectToLoginPage(): void {
|
||||||
this.authenticationService.removeLocalStorageData();
|
this.authenticationService.removeLocalStorageData();
|
||||||
|
void this.router.navigateByUrl('/logga-in');
|
||||||
if (environment.ciam.clientId) {
|
|
||||||
window.location.href = `${environment.ciam.loginUrl}&client_id=${environment.ciam.clientId}&redirect_uri=${window.location.origin}`;
|
|
||||||
} else {
|
|
||||||
void this.router.navigateByUrl(environment.ciam.loginUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CanActivate, Router } from '@angular/router';
|
||||||
|
import { AuthenticationService } from '@msfa-services/api/authentication.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class SkipIfLoggedInGuard implements CanActivate {
|
||||||
|
constructor(private authenticationService: AuthenticationService, private router: Router) {}
|
||||||
|
|
||||||
|
canActivate(): boolean {
|
||||||
|
if (this.authenticationService.isLoggedInWithValidToken) {
|
||||||
|
void this.router.navigateByUrl('/');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import { environment } from '@msfa-environment';
|
|||||||
import { AuthenticationResponse } from '@msfa-models/api/authentication.response.model';
|
import { AuthenticationResponse } from '@msfa-models/api/authentication.response.model';
|
||||||
import { Authentication, mapAuthApiResponseToAuthenticationResult } from '@msfa-models/authentication.model';
|
import { Authentication, mapAuthApiResponseToAuthenticationResult } from '@msfa-models/authentication.model';
|
||||||
import { add, isAfter, isBefore, sub } from 'date-fns';
|
import { add, isAfter, isBefore, sub } from 'date-fns';
|
||||||
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
|
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||||
import { catchError, distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
|
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -29,33 +29,32 @@ export class AuthenticationService {
|
|||||||
map(token => !!token)
|
map(token => !!token)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
get isLoggedIn(): boolean {
|
||||||
|
return !!this._token$.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
// We set token validity at 1 minute prior expiration time.
|
// We set token validity at 1 minute prior expiration time.
|
||||||
// Refresh will be possible until expiration time.
|
// Refresh will be possible until expiration time.
|
||||||
// TODO: Refesh solution
|
// TODO: Refesh solution
|
||||||
isTokenValid$: Observable<{ isValid: boolean; isRefreshable: boolean }> = combineLatest([
|
get isTokenValid(): boolean {
|
||||||
this._token$,
|
|
||||||
this._expiresAt$,
|
|
||||||
]).pipe(
|
|
||||||
filter(([token, expiresAt]) => !!(token && expiresAt)),
|
|
||||||
map(([, expiresAt]) => {
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const expiresAt = this._expiresAt$.getValue();
|
||||||
const applicationClosedTimeStamp = localStorage.getItem(APPLICATION_CLOSED_TIME_STAMP);
|
const applicationClosedTimeStamp = localStorage.getItem(APPLICATION_CLOSED_TIME_STAMP);
|
||||||
// Checking to see if the user has been active on the page within the last hour
|
// Checking to see if the user has been active on the page within the last hour
|
||||||
const applicationClosedWithin1Hour =
|
const applicationClosedWithin1Hour =
|
||||||
!applicationClosedTimeStamp || isAfter(+applicationClosedTimeStamp, sub(now, { hours: 1 }));
|
!applicationClosedTimeStamp || isAfter(+applicationClosedTimeStamp, sub(now, { hours: 1 }));
|
||||||
const isValid = isBefore(now, sub(expiresAt, { minutes: 1 })) && applicationClosedWithin1Hour;
|
return isBefore(now, sub(expiresAt, { minutes: 1 })) && applicationClosedWithin1Hour;
|
||||||
|
|
||||||
if (applicationClosedTimeStamp) {
|
|
||||||
localStorage.removeItem(APPLICATION_CLOSED_TIME_STAMP);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
get isTokenRefreshable(): boolean {
|
||||||
isValid,
|
// const expiresAt = this._expiresAt$.getValue();
|
||||||
isRefreshable: false,
|
// return isBefore(new Date(), expiresAt);
|
||||||
// isRefreshable: isBefore(new Date(), expiresAt),
|
return false;
|
||||||
};
|
}
|
||||||
})
|
|
||||||
);
|
get isLoggedInWithValidToken(): boolean {
|
||||||
|
return this.isLoggedIn && this.isTokenValid;
|
||||||
|
}
|
||||||
|
|
||||||
private static _authTokenApiUrl(code: string): string {
|
private static _authTokenApiUrl(code: string): string {
|
||||||
return `${environment.api.url}/auth/token?accessCode=${code}`;
|
return `${environment.api.url}/auth/token?accessCode=${code}`;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
|
||||||
import { BehaviorSubject, fromEvent, merge, Observable } from 'rxjs';
|
import { BehaviorSubject, fromEvent, merge, Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { AuthenticationService } from './authentication.service';
|
import { AuthenticationService } from './api/authentication.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -21,7 +21,6 @@ export class IdleService extends UnsubscribeDirective {
|
|||||||
private _isIdle$ = new BehaviorSubject<boolean>(false);
|
private _isIdle$ = new BehaviorSubject<boolean>(false);
|
||||||
public isIdle$: Observable<boolean> = this._isIdle$.asObservable();
|
public isIdle$: Observable<boolean> = this._isIdle$.asObservable();
|
||||||
private _isActive$ = new BehaviorSubject<boolean>(true);
|
private _isActive$ = new BehaviorSubject<boolean>(true);
|
||||||
public isActive$: Observable<boolean> = this._isActive$.asObservable();
|
|
||||||
private _timeLeftBeforeLogoutInSeconds$ = new BehaviorSubject<number>(2 * 60);
|
private _timeLeftBeforeLogoutInSeconds$ = new BehaviorSubject<number>(2 * 60);
|
||||||
|
|
||||||
private _idleAfterMS = 10 * 60 * 1000; // 10 minutes
|
private _idleAfterMS = 10 * 60 * 1000; // 10 minutes
|
||||||
@@ -41,7 +40,7 @@ export class IdleService extends UnsubscribeDirective {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _setNewTimeoutIfActive(): void {
|
private _setNewTimeoutIfActive(): void {
|
||||||
if (this._isActive$.getValue()) {
|
if (this._isActive$.getValue() && this.authenticationService.isLoggedInWithValidToken) {
|
||||||
this._clearTimeouts();
|
this._clearTimeouts();
|
||||||
this._setIdleTimeout();
|
this._setIdleTimeout();
|
||||||
}
|
}
|
||||||
@@ -16,31 +16,49 @@
|
|||||||
outline: var(--digi-button--outline);
|
outline: var(--digi-button--outline);
|
||||||
border-color: var(--digi-button--border-color);
|
border-color: var(--digi-button--border-color);
|
||||||
|
|
||||||
@if $type == 'secondary' {
|
|
||||||
background-color: var(--digi-button--background--secondary);
|
|
||||||
color: var(--digi-button--color--secondary);
|
|
||||||
} @else if $type == 'tertiary' {
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--digi-button--color--tertiary);
|
|
||||||
border-width: 0;
|
|
||||||
} @else {
|
|
||||||
background-color: var(--digi-button--background);
|
|
||||||
color: var(--digi-button--color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: var(--digi-button--outline--focus);
|
outline: var(--digi-button--outline--focus);
|
||||||
border-color: var(--digi-button--border-color--hover);
|
border-color: var(--digi-button--border-color--hover);
|
||||||
|
}
|
||||||
|
|
||||||
@if $type == 'secondary' {
|
&--primary {
|
||||||
background-color: var(--digi-button--background--secondary--hover);
|
background-color: var(--digi-button--background);
|
||||||
color: var(--digi-button--color--secondary);
|
color: var(--digi-button--color);
|
||||||
} @else if $type == 'tertiary' {
|
|
||||||
color: var(--digi-button--color--tertiary--hover);
|
&:hover,
|
||||||
} @else {
|
&:focus {
|
||||||
background-color: var(--digi-button--background--hover);
|
background-color: var(--digi-button--background--hover);
|
||||||
color: var(--digi-button--color--hover);
|
color: var(--digi-button--color--hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&--secondary {
|
||||||
|
background-color: var(--digi-button--background--secondary);
|
||||||
|
color: var(--digi-button--color--secondary);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--digi-button--background--secondary--hover);
|
||||||
|
color: var(--digi-button--color--secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--tertiary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--digi-button--color--tertiary);
|
||||||
|
border-width: 0;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: var(--digi-button--color--tertiary--hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--s {
|
||||||
|
padding: var(--digi-button--padding--s);
|
||||||
|
font-size: var(--digi-button--font-size--s);
|
||||||
|
}
|
||||||
|
&--l {
|
||||||
|
padding: var(--digi-button--padding--l);
|
||||||
|
font-size: var(--digi-button--font-size--l);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
<a [ngClass]="linkButtonClass" [routerLink]="uiRouterLink" [queryParams]="uiQueryParams">
|
<a
|
||||||
<ng-content></ng-content>
|
*ngIf="uiRouterLink; else externalLinkRef"
|
||||||
|
[ngClass]="linkButtonClass"
|
||||||
|
[routerLink]="uiRouterLink"
|
||||||
|
[queryParams]="uiQueryParams"
|
||||||
|
>
|
||||||
|
<ng-container *ngTemplateOutlet="contentRef"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<ng-template #externalLinkRef>
|
||||||
|
<a *ngIf="uiHref" [ngClass]="linkButtonClass" [attr.href]="uiHref">
|
||||||
|
<ng-container *ngTemplateOutlet="contentRef"></ng-container>
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #contentRef>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</ng-template>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
@import 'mixins/buttons';
|
@import 'mixins/buttons';
|
||||||
|
|
||||||
.ui-link-button {
|
.ui-link-button {
|
||||||
&--primary {
|
|
||||||
@include msfa__button;
|
@include msfa__button;
|
||||||
}
|
|
||||||
&--secondary {
|
&--full-width {
|
||||||
@include msfa__button('secondary');
|
width: 100%;
|
||||||
}
|
|
||||||
&--tertiary {
|
|
||||||
@include msfa__button('tertiary');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,21 @@ import { UiLinkButtonType } from './link-button-type.enum';
|
|||||||
export class LinkButtonComponent {
|
export class LinkButtonComponent {
|
||||||
private readonly _defaultClass = 'ui-link-button';
|
private readonly _defaultClass = 'ui-link-button';
|
||||||
@Input() uiType: UiLinkButtonType = UiLinkButtonType.PRIMARY;
|
@Input() uiType: UiLinkButtonType = UiLinkButtonType.PRIMARY;
|
||||||
@Input() uiSize: 's' | 'm' = 'm';
|
@Input() uiSize: 's' | 'm' | 'l' = 'm';
|
||||||
|
@Input() uiFullWidth = false;
|
||||||
@Input() uiRouterLink: string | string[];
|
@Input() uiRouterLink: string | string[];
|
||||||
|
@Input() uiHref: string;
|
||||||
@Input() uiQueryParams: Params = null;
|
@Input() uiQueryParams: Params = null;
|
||||||
|
|
||||||
get linkButtonClass(): string {
|
get linkButtonClass(): string {
|
||||||
|
let currentClass = `${this._defaultClass} ${this._defaultClass}--${this.uiSize}`;
|
||||||
|
|
||||||
|
if (this.uiFullWidth) {
|
||||||
|
currentClass = `${currentClass} ${this._defaultClass}--full-width`;
|
||||||
|
}
|
||||||
if (this.uiType) {
|
if (this.uiType) {
|
||||||
return `${this._defaultClass} ${this._defaultClass}--${this.uiType as string}`;
|
currentClass = `${currentClass} ${this._defaultClass}--${this.uiType as string}`;
|
||||||
}
|
}
|
||||||
return this._defaultClass;
|
return currentClass;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user