diff --git a/CHANGELOG.md b/CHANGELOG.md index f99577be2..d3560c261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Improved the sign in with fingerprint + ## 1.15.0 - 14.06.2021 ### Added diff --git a/apps/api/src/app/auth/web-auth.service.ts b/apps/api/src/app/auth/web-auth.service.ts index 7b3fd813a..157f75e36 100644 --- a/apps/api/src/app/auth/web-auth.service.ts +++ b/apps/api/src/app/auth/web-auth.service.ts @@ -57,8 +57,9 @@ export class WebAuthService { timeout: 60000, attestationType: 'indirect', authenticatorSelection: { - userVerification: 'preferred', - requireResidentKey: false + authenticatorAttachment: 'platform', + requireResidentKey: false, + userVerification: 'required' } }; @@ -143,7 +144,7 @@ export class WebAuthService { { id: device.credentialId, type: 'public-key', - transports: ['usb', 'ble', 'nfc', 'internal'] + transports: ['internal'] } ], userVerification: 'preferred', diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index 51dcaf4f5..9faf0553f 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -92,6 +92,13 @@ const routes: Routes = [ (m) => m.TransactionsPageModule ) }, + { + path: 'webauthn', + loadChildren: () => + import('./pages/webauthn/webauthn-page.module').then( + (m) => m.WebauthnPageModule + ) + }, { path: 'zen', loadChildren: () => diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 6be6706a6..9e1bec14e 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -11,6 +11,10 @@ import { Router } from '@angular/router'; import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; +import { + STAY_SIGNED_IN, + SettingsStorageService +} from '@ghostfolio/client/services/settings-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { InfoItem, User } from '@ghostfolio/common/interfaces'; @@ -43,8 +47,8 @@ export class HeaderComponent implements OnChanges { private dialog: MatDialog, private impersonationStorageService: ImpersonationStorageService, private router: Router, - private tokenStorageService: TokenStorageService, - private webAuthnService: WebAuthnService + private settingsStorageService: SettingsStorageService, + private tokenStorageService: TokenStorageService ) { this.impersonationStorageService .onChangeHasImpersonation() @@ -108,14 +112,17 @@ export class HeaderComponent implements OnChanges { takeUntil(this.unsubscribeSubject) ) .subscribe(({ authToken }) => { - this.setToken(authToken, data.staySignedIn); + this.setToken(authToken); }); } }); } - public setToken(aToken: string, staySignedIn: boolean) { - this.tokenStorageService.saveToken(aToken, staySignedIn); + public setToken(aToken: string) { + this.tokenStorageService.saveToken( + aToken, + this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true' + ); this.router.navigate(['/']); } diff --git a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts index 0de670f92..a5579df7f 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts +++ b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts @@ -1,5 +1,10 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { MatCheckboxChange } from '@angular/material/checkbox'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + STAY_SIGNED_IN, + SettingsStorageService +} from '@ghostfolio/client/services/settings-storage.service'; @Component({ selector: 'gf-login-with-access-token-dialog', @@ -9,13 +14,21 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; }) export class LoginWithAccessTokenDialog { public constructor( + @Inject(MAT_DIALOG_DATA) public data: any, public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any + private settingsStorageService: SettingsStorageService ) {} ngOnInit() {} - public onClose(): void { + public onChangeStaySignedIn(aValue: MatCheckboxChange) { + this.settingsStorageService.setSetting( + STAY_SIGNED_IN, + aValue.checked?.toString() + ); + } + + public onClose() { this.dialogRef.close(); } } diff --git a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html index 3040d5e08..614c28bfe 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html +++ b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -28,7 +28,7 @@
- Stay signed in
diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index 3d807bfd3..50489d10a 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -79,10 +79,7 @@ export class HttpResponseInterceptor implements HttpInterceptor { } } else if (error.status === StatusCodes.UNAUTHORIZED) { if (this.webAuthnService.isEnabled()) { - this.webAuthnService.login().subscribe(({ authToken }) => { - this.tokenStorageService.saveToken(authToken, false); - window.location.reload(); - }); + this.router.navigate(['/webauthn']); } else { this.tokenStorageService.signOut(); } diff --git a/apps/client/src/app/pages/auth/auth-page.component.ts b/apps/client/src/app/pages/auth/auth-page.component.ts index 70f4f6563..e1a1877a4 100644 --- a/apps/client/src/app/pages/auth/auth-page.component.ts +++ b/apps/client/src/app/pages/auth/auth-page.component.ts @@ -1,5 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { + STAY_SIGNED_IN, + SettingsStorageService +} from '@ghostfolio/client/services/settings-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; @Component({ @@ -14,6 +18,7 @@ export class AuthPageComponent implements OnInit { public constructor( private route: ActivatedRoute, private router: Router, + private settingsStorageService: SettingsStorageService, private tokenStorageService: TokenStorageService ) {} @@ -23,7 +28,10 @@ export class AuthPageComponent implements OnInit { public ngOnInit() { this.route.params.subscribe((params) => { const jwt = params['jwt']; - this.tokenStorageService.saveToken(jwt); + this.tokenStorageService.saveToken( + jwt, + this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true' + ); this.router.navigate(['/']); }); diff --git a/apps/client/src/app/pages/landing/landing-page.component.ts b/apps/client/src/app/pages/landing/landing-page.component.ts index a8b1e3c6d..1ff3717ab 100644 --- a/apps/client/src/app/pages/landing/landing-page.component.ts +++ b/apps/client/src/app/pages/landing/landing-page.component.ts @@ -26,8 +26,7 @@ export class LandingPageComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private router: Router, - private tokenStorageService: TokenStorageService, - private webAuthnService: WebAuthnService + private tokenStorageService: TokenStorageService ) {} /** @@ -257,7 +256,7 @@ export class LandingPageComponent implements OnDestroy, OnInit { } public setToken(aToken: string) { - this.tokenStorageService.saveToken(aToken); + this.tokenStorageService.saveToken(aToken, true); this.router.navigate(['/']); } diff --git a/apps/client/src/app/pages/register/register-page.component.ts b/apps/client/src/app/pages/register/register-page.component.ts index c328f832d..16668b003 100644 --- a/apps/client/src/app/pages/register/register-page.component.ts +++ b/apps/client/src/app/pages/register/register-page.component.ts @@ -78,19 +78,13 @@ export class RegisterPageComponent implements OnDestroy, OnInit { dialogRef.afterClosed().subscribe((data) => { if (data?.authToken) { - this.tokenStorageService.saveToken(authToken); + this.tokenStorageService.saveToken(authToken, true); this.router.navigate(['/']); } }); } - public setToken(aToken: string) { - this.tokenStorageService.saveToken(aToken); - - this.router.navigate(['/']); - } - public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts b/apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts new file mode 100644 index 000000000..35bfb2580 --- /dev/null +++ b/apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component'; + +const routes: Routes = [{ path: '', component: WebauthnPageComponent }]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class WebauthnPageRoutingModule {} diff --git a/apps/client/src/app/pages/webauthn/webauthn-page.component.ts b/apps/client/src/app/pages/webauthn/webauthn-page.component.ts new file mode 100644 index 000000000..431e4bc81 --- /dev/null +++ b/apps/client/src/app/pages/webauthn/webauthn-page.component.ts @@ -0,0 +1,46 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; + +@Component({ + selector: 'gf-webauthn-page', + templateUrl: './webauthn-page.html', + styleUrls: ['./webauthn-page.scss'] +}) +export class WebauthnPageComponent implements OnInit { + public hasError = false; + + constructor( + private changeDetectorRef: ChangeDetectorRef, + private router: Router, + private tokenStorageService: TokenStorageService, + private webAuthnService: WebAuthnService + ) {} + + public ngOnInit() { + this.signIn(); + } + + public deregisterDevice() { + this.webAuthnService.deregister().subscribe(() => { + this.router.navigate(['/']); + }); + } + + public signIn() { + this.hasError = false; + + this.webAuthnService.login().subscribe( + ({ authToken }) => { + this.tokenStorageService.saveToken(authToken, false); + this.router.navigate(['/']); + }, + (error) => { + console.error(error); + this.hasError = true; + this.changeDetectorRef.markForCheck(); + } + ); + } +} diff --git a/apps/client/src/app/pages/webauthn/webauthn-page.html b/apps/client/src/app/pages/webauthn/webauthn-page.html new file mode 100644 index 000000000..02650bd7c --- /dev/null +++ b/apps/client/src/app/pages/webauthn/webauthn-page.html @@ -0,0 +1,27 @@ +
+
+
+ +
+
+

+ Oops, authentication failed +

+ + +
+
+
diff --git a/apps/client/src/app/pages/webauthn/webauthn-page.module.ts b/apps/client/src/app/pages/webauthn/webauthn-page.module.ts new file mode 100644 index 000000000..01a7dcd4b --- /dev/null +++ b/apps/client/src/app/pages/webauthn/webauthn-page.module.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component'; + +import { WebauthnPageRoutingModule } from './webauthn-page-routing.module'; + +@NgModule({ + declarations: [WebauthnPageComponent], + exports: [], + imports: [ + CommonModule, + MatButtonModule, + MatProgressSpinnerModule, + WebauthnPageRoutingModule + ], + providers: [] +}) +export class WebauthnPageModule {} diff --git a/apps/client/src/app/pages/webauthn/webauthn-page.scss b/apps/client/src/app/pages/webauthn/webauthn-page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/client/src/app/services/settings-storage.service.ts b/apps/client/src/app/services/settings-storage.service.ts index f195ee38a..a340f89b7 100644 --- a/apps/client/src/app/services/settings-storage.service.ts +++ b/apps/client/src/app/services/settings-storage.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; export const RANGE = 'range'; +export const STAY_SIGNED_IN = 'staySignedIn'; @Injectable({ providedIn: 'root' diff --git a/apps/client/src/app/services/token-storage.service.ts b/apps/client/src/app/services/token-storage.service.ts index 1fd409926..5980f56e1 100644 --- a/apps/client/src/app/services/token-storage.service.ts +++ b/apps/client/src/app/services/token-storage.service.ts @@ -21,7 +21,7 @@ export class TokenStorageService { ); } - public saveToken(token: string, staySignedIn: boolean = false): void { + public saveToken(token: string, staySignedIn = false): void { if (staySignedIn) { window.localStorage.setItem(TOKEN_KEY, token); }