diff --git a/apps/mina-sidor-fa/src/app/app-routing.module.ts b/apps/mina-sidor-fa/src/app/app-routing.module.ts index f6fe619..1659717 100644 --- a/apps/mina-sidor-fa/src/app/app-routing.module.ts +++ b/apps/mina-sidor-fa/src/app/app-routing.module.ts @@ -6,6 +6,7 @@ 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'; +import { SkipIfLoggedInGuard } from '@msfa-guards/skip-if-logged-in.guard'; const activeFeatures: Feature[] = environment.activeFeatures; @@ -16,6 +17,12 @@ const routes: Routes = [ loadChildren: () => import('./pages/start/start.module').then(m => m.StartModule), 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', data: { title: 'Logga ut' }, diff --git a/apps/mina-sidor-fa/src/app/app.component.ts b/apps/mina-sidor-fa/src/app/app.component.ts index cf3fc21..6362df5 100644 --- a/apps/mina-sidor-fa/src/app/app.component.ts +++ b/apps/mina-sidor-fa/src/app/app.component.ts @@ -5,7 +5,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { APPLICATION_CLOSED_TIME_STAMP } from '@msfa-constants/local-storage-keys'; import { environment } from '@msfa-environment'; 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 { filter, map, switchMap } from 'rxjs/operators'; diff --git a/apps/mina-sidor-fa/src/app/pages/login/login.component.html b/apps/mina-sidor-fa/src/app/pages/login/login.component.html new file mode 100644 index 0000000..95324c0 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/login/login.component.html @@ -0,0 +1,48 @@ + + + + + Hur vill du logga in? + + + + + + + Mobilt bank-id + + + + + + Bank-id + + + + + + Användarnamn och lösenord + + + + + + + + diff --git a/apps/mina-sidor-fa/src/app/pages/login/login.component.scss b/apps/mina-sidor-fa/src/app/pages/login/login.component.scss new file mode 100644 index 0000000..1cc7a37 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/login/login.component.scss @@ -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; + } +} diff --git a/apps/mina-sidor-fa/src/app/pages/login/login.component.spec.ts b/apps/mina-sidor-fa/src/app/pages/login/login.component.spec.ts new file mode 100644 index 0000000..8398812 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/login/login.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/apps/mina-sidor-fa/src/app/pages/login/login.component.ts b/apps/mina-sidor-fa/src/app/pages/login/login.component.ts new file mode 100644 index 0000000..5c61a44 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/login/login.component.ts @@ -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`; +} diff --git a/apps/mina-sidor-fa/src/app/pages/login/login.module.ts b/apps/mina-sidor-fa/src/app/pages/login/login.module.ts new file mode 100644 index 0000000..565533e --- /dev/null +++ b/apps/mina-sidor-fa/src/app/pages/login/login.module.ts @@ -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 {} diff --git a/apps/mina-sidor-fa/src/app/pages/my-account/my-account.component.html b/apps/mina-sidor-fa/src/app/pages/my-account/my-account.component.html index 2f19ef0..91c02c9 100644 --- a/apps/mina-sidor-fa/src/app/pages/my-account/my-account.component.html +++ b/apps/mina-sidor-fa/src/app/pages/my-account/my-account.component.html @@ -4,10 +4,10 @@ Mitt konto - + Logga ut - + diff --git a/apps/mina-sidor-fa/src/app/pages/my-account/my-account.module.ts b/apps/mina-sidor-fa/src/app/pages/my-account/my-account.module.ts index 697bb72..502d19d 100644 --- a/apps/mina-sidor-fa/src/app/pages/my-account/my-account.module.ts +++ b/apps/mina-sidor-fa/src/app/pages/my-account/my-account.module.ts @@ -1,4 +1,3 @@ -import { UiSkeletonModule } from '@ui/skeleton/skeleton.module'; import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 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 { OrganizationPickerFormModule } from '@msfa-shared/components/organization-picker-form/organization-picker-form.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'; @NgModule({ @@ -21,6 +22,7 @@ import { MyAccountComponent } from './my-account.component'; OrganizationPickerFormModule, RolesDialogModule, HideTextModule, + UiLinkButtonModule, ], }) export class MyAccountModule {} diff --git a/apps/mina-sidor-fa/src/app/shared/components/icon/icon.component.html b/apps/mina-sidor-fa/src/app/shared/components/icon/icon.component.html index 80fbd8c..96f3f33 100644 --- a/apps/mina-sidor-fa/src/app/shared/components/icon/icon.component.html +++ b/apps/mina-sidor-fa/src/app/shared/components/icon/icon.component.html @@ -1,4 +1,11 @@ + + + + diff --git a/apps/mina-sidor-fa/src/app/shared/components/layout/layout.component.ts b/apps/mina-sidor-fa/src/app/shared/components/layout/layout.component.ts index 2ad90d1..3527e14 100644 --- a/apps/mina-sidor-fa/src/app/shared/components/layout/layout.component.ts +++ b/apps/mina-sidor-fa/src/app/shared/components/layout/layout.component.ts @@ -19,7 +19,6 @@ import { filter } from 'rxjs/operators'; }) export class LayoutComponent extends UnsubscribeDirective { @Input() showBreadCrumbs = true; - isLoggedIn$: Observable = this.authenticationService.isLoggedIn$; selectedOrganization$: Observable = this.userService.selectedOrganization$; user$: Observable = this.userService.user$.pipe(filter(user => !!user)); roles$: Observable = this.userService.userRoles$.pipe(filter(roles => !!roles)); diff --git a/apps/mina-sidor-fa/src/app/shared/enums/icon-type.enum.ts b/apps/mina-sidor-fa/src/app/shared/enums/icon-type.enum.ts index 5707b41..d02a872 100644 --- a/apps/mina-sidor-fa/src/app/shared/enums/icon-type.enum.ts +++ b/apps/mina-sidor-fa/src/app/shared/enums/icon-type.enum.ts @@ -5,6 +5,7 @@ export enum IconType { CLIPBOARD = 'clipboard', // Custom BUILDING = 'building', // Custom LOGOUT = 'logout', // Custom + BANKID = 'bankid', // Custom USER = 'user', USERS = 'users', BELL = 'bell', diff --git a/apps/mina-sidor-fa/src/app/shared/guards/auth.guard.ts b/apps/mina-sidor-fa/src/app/shared/guards/auth.guard.ts index 0b9c774..edc4038 100644 --- a/apps/mina-sidor-fa/src/app/shared/guards/auth.guard.ts +++ b/apps/mina-sidor-fa/src/app/shared/guards/auth.guard.ts @@ -1,9 +1,8 @@ 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 { Observable, of } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -12,38 +11,27 @@ export class AuthGuard implements CanActivate { constructor(private authenticationService: AuthenticationService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot): Observable { - return this.authenticationService.isLoggedIn$.pipe( - switchMap(loggedIn => { - if (loggedIn) { - return this.authenticationService.isTokenValid$.pipe( - switchMap(({ isValid, isRefreshable }) => { - if (isValid) { - return of(true); - } - if (isRefreshable) { - return this.authenticationService.refreshToken$(); - } - this.redirectToLoginPage(); - return of(false); - }) - ); - } else if (route.queryParams.code) { - return this.authenticationService.login$(route.queryParams.code).pipe(map(result => !!result)); - } + const isLoggedIn = this.authenticationService.isLoggedIn; + const isTokenValid = this.authenticationService.isTokenValid; + const isTokenRefreshable = this.authenticationService.isTokenRefreshable; - this.redirectToLoginPage(); - return of(false); - }) - ); + if (isLoggedIn) { + if (isTokenValid) { + return of(true); + } + if (isTokenRefreshable) { + return this.authenticationService.refreshToken$(); + } + } else if (route.queryParams.code) { + return this.authenticationService.login$(route.queryParams.code).pipe(map(result => !!result)); + } + + this.redirectToLoginPage(); + return of(false); } redirectToLoginPage(): void { this.authenticationService.removeLocalStorageData(); - - 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); - } + void this.router.navigateByUrl('/logga-in'); } } diff --git a/apps/mina-sidor-fa/src/app/shared/guards/skip-if-logged-in.guard.ts b/apps/mina-sidor-fa/src/app/shared/guards/skip-if-logged-in.guard.ts new file mode 100644 index 0000000..8a3a259 --- /dev/null +++ b/apps/mina-sidor-fa/src/app/shared/guards/skip-if-logged-in.guard.ts @@ -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; + } +} diff --git a/apps/mina-sidor-fa/src/app/shared/services/api/authentication.service.ts b/apps/mina-sidor-fa/src/app/shared/services/api/authentication.service.ts index 34c3e09..89d8d7a 100644 --- a/apps/mina-sidor-fa/src/app/shared/services/api/authentication.service.ts +++ b/apps/mina-sidor-fa/src/app/shared/services/api/authentication.service.ts @@ -12,8 +12,8 @@ import { environment } from '@msfa-environment'; import { AuthenticationResponse } from '@msfa-models/api/authentication.response.model'; import { Authentication, mapAuthApiResponseToAuthenticationResult } from '@msfa-models/authentication.model'; import { add, isAfter, isBefore, sub } from 'date-fns'; -import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; -import { catchError, distinctUntilChanged, filter, map, tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -29,33 +29,32 @@ export class AuthenticationService { map(token => !!token) ); + get isLoggedIn(): boolean { + return !!this._token$.getValue(); + } + // We set token validity at 1 minute prior expiration time. // Refresh will be possible until expiration time. // TODO: Refesh solution - isTokenValid$: Observable<{ isValid: boolean; isRefreshable: boolean }> = combineLatest([ - this._token$, - this._expiresAt$, - ]).pipe( - filter(([token, expiresAt]) => !!(token && expiresAt)), - map(([, expiresAt]) => { - const now = new Date(); - const applicationClosedTimeStamp = localStorage.getItem(APPLICATION_CLOSED_TIME_STAMP); - // Checking to see if the user has been active on the page within the last hour - const applicationClosedWithin1Hour = - !applicationClosedTimeStamp || isAfter(+applicationClosedTimeStamp, sub(now, { hours: 1 })); - const isValid = isBefore(now, sub(expiresAt, { minutes: 1 })) && applicationClosedWithin1Hour; + get isTokenValid(): boolean { + const now = new Date(); + const expiresAt = this._expiresAt$.getValue(); + const applicationClosedTimeStamp = localStorage.getItem(APPLICATION_CLOSED_TIME_STAMP); + // Checking to see if the user has been active on the page within the last hour + const applicationClosedWithin1Hour = + !applicationClosedTimeStamp || isAfter(+applicationClosedTimeStamp, sub(now, { hours: 1 })); + return isBefore(now, sub(expiresAt, { minutes: 1 })) && applicationClosedWithin1Hour; + } - if (applicationClosedTimeStamp) { - localStorage.removeItem(APPLICATION_CLOSED_TIME_STAMP); - } + get isTokenRefreshable(): boolean { + // const expiresAt = this._expiresAt$.getValue(); + // return isBefore(new Date(), expiresAt); + return false; + } - return { - isValid, - isRefreshable: false, - // isRefreshable: isBefore(new Date(), expiresAt), - }; - }) - ); + get isLoggedInWithValidToken(): boolean { + return this.isLoggedIn && this.isTokenValid; + } private static _authTokenApiUrl(code: string): string { return `${environment.api.url}/auth/token?accessCode=${code}`; diff --git a/apps/mina-sidor-fa/src/app/shared/services/api/idle.service.ts b/apps/mina-sidor-fa/src/app/shared/services/idle.service.ts similarity index 94% rename from apps/mina-sidor-fa/src/app/shared/services/api/idle.service.ts rename to apps/mina-sidor-fa/src/app/shared/services/idle.service.ts index 527a43b..a98111a 100644 --- a/apps/mina-sidor-fa/src/app/shared/services/api/idle.service.ts +++ b/apps/mina-sidor-fa/src/app/shared/services/idle.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive'; import { BehaviorSubject, fromEvent, merge, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { AuthenticationService } from './authentication.service'; +import { AuthenticationService } from './api/authentication.service'; @Injectable({ providedIn: 'root', @@ -21,7 +21,6 @@ export class IdleService extends UnsubscribeDirective { private _isIdle$ = new BehaviorSubject(false); public isIdle$: Observable = this._isIdle$.asObservable(); private _isActive$ = new BehaviorSubject(true); - public isActive$: Observable = this._isActive$.asObservable(); private _timeLeftBeforeLogoutInSeconds$ = new BehaviorSubject(2 * 60); private _idleAfterMS = 10 * 60 * 1000; // 10 minutes @@ -41,7 +40,7 @@ export class IdleService extends UnsubscribeDirective { } private _setNewTimeoutIfActive(): void { - if (this._isActive$.getValue()) { + if (this._isActive$.getValue() && this.authenticationService.isLoggedInWithValidToken) { this._clearTimeouts(); this._setIdleTimeout(); } diff --git a/libs/styles/src/mixins/_buttons.scss b/libs/styles/src/mixins/_buttons.scss index 0893ffc..b71c88f 100644 --- a/libs/styles/src/mixins/_buttons.scss +++ b/libs/styles/src/mixins/_buttons.scss @@ -16,31 +16,49 @@ outline: var(--digi-button--outline); 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, &:focus { outline: var(--digi-button--outline--focus); border-color: var(--digi-button--border-color--hover); + } - @if $type == 'secondary' { - background-color: var(--digi-button--background--secondary--hover); - color: var(--digi-button--color--secondary); - } @else if $type == 'tertiary' { - color: var(--digi-button--color--tertiary--hover); - } @else { + &--primary { + background-color: var(--digi-button--background); + color: var(--digi-button--color); + + &:hover, + &:focus { background-color: var(--digi-button--background--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); + } } diff --git a/libs/ui/src/link-button/link-button.component.html b/libs/ui/src/link-button/link-button.component.html index 0f2c228..ec62958 100644 --- a/libs/ui/src/link-button/link-button.component.html +++ b/libs/ui/src/link-button/link-button.component.html @@ -1,3 +1,18 @@ - - + + + + + + + + + + + + diff --git a/libs/ui/src/link-button/link-button.component.scss b/libs/ui/src/link-button/link-button.component.scss index 9e835f8..4678c2a 100644 --- a/libs/ui/src/link-button/link-button.component.scss +++ b/libs/ui/src/link-button/link-button.component.scss @@ -1,13 +1,9 @@ @import 'mixins/buttons'; .ui-link-button { - &--primary { - @include msfa__button; - } - &--secondary { - @include msfa__button('secondary'); - } - &--tertiary { - @include msfa__button('tertiary'); + @include msfa__button; + + &--full-width { + width: 100%; } } diff --git a/libs/ui/src/link-button/link-button.component.ts b/libs/ui/src/link-button/link-button.component.ts index b2f176b..42bb9e4 100644 --- a/libs/ui/src/link-button/link-button.component.ts +++ b/libs/ui/src/link-button/link-button.component.ts @@ -11,14 +11,21 @@ import { UiLinkButtonType } from './link-button-type.enum'; export class LinkButtonComponent { private readonly _defaultClass = 'ui-link-button'; @Input() uiType: UiLinkButtonType = UiLinkButtonType.PRIMARY; - @Input() uiSize: 's' | 'm' = 'm'; + @Input() uiSize: 's' | 'm' | 'l' = 'm'; + @Input() uiFullWidth = false; @Input() uiRouterLink: string | string[]; + @Input() uiHref: string; @Input() uiQueryParams: Params = null; get linkButtonClass(): string { - if (this.uiType) { - return `${this._defaultClass} ${this._defaultClass}--${this.uiType as string}`; + let currentClass = `${this._defaultClass} ${this._defaultClass}--${this.uiSize}`; + + if (this.uiFullWidth) { + currentClass = `${currentClass} ${this._defaultClass}--full-width`; } - return this._defaultClass; + if (this.uiType) { + currentClass = `${currentClass} ${this._defaultClass}--${this.uiType as string}`; + } + return currentClass; } }