Browse Source

Improve usability

pull/82/head
Thomas 4 years ago
parent
commit
4e6920b6d1
  1. 3
      apps/client/src/app/components/header/header.component.ts
  2. 11
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts
  3. 16
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  4. 2
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.module.ts
  5. 10
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.scss
  6. 72
      apps/client/src/app/pages/account/account-page.component.ts
  7. 27
      apps/client/src/app/pages/account/account-page.html
  8. 2
      apps/client/src/app/pages/account/account-page.module.ts
  9. 8
      apps/client/src/app/services/web-authn.service.ts

3
apps/client/src/app/components/header/header.component.ts

@ -89,7 +89,8 @@ export class HeaderComponent implements OnChanges {
autoFocus: false, autoFocus: false,
data: { data: {
accessToken: '', accessToken: '',
hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin,
title: 'Sign in'
}, },
width: '30rem' width: '30rem'
}); });

11
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({ @Component({
selector: 'gf-login-with-access-token-dialog', selector: 'gf-login-with-access-token-dialog',
@ -8,7 +8,14 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
templateUrl: 'login-with-access-token-dialog.html' templateUrl: 'login-with-access-token-dialog.html'
}) })
export class LoginWithAccessTokenDialog { export class LoginWithAccessTokenDialog {
public constructor(@Inject(MAT_DIALOG_DATA) public data: any) {} public constructor(
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {}
ngOnInit() {} ngOnInit() {}
public onClose(): void {
this.dialogRef.close();
}
} }

16
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -1,4 +1,9 @@
<h1 mat-dialog-title i18n>Sign in</h1> <gf-dialog-header
mat-dialog-title
[title]="data.title"
(closeButtonClicked)="onClose()"
></gf-dialog-header>
<div mat-dialog-content> <div mat-dialog-content>
<div> <div>
<ng-container *ngIf="data.hasPermissionToUseSocialLogin"> <ng-container *ngIf="data.hasPermissionToUseSocialLogin">
@ -19,13 +24,15 @@
[(ngModel)]="data.accessToken" [(ngModel)]="data.accessToken"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
</div>
</div>
<div mat-dialog-actions>
<div class="flex-grow-1">
<mat-checkbox i18n [(ngModel)]="data.staySignedIn" <mat-checkbox i18n [(ngModel)]="data.staySignedIn"
>Stay signed in</mat-checkbox >Stay signed in</mat-checkbox
> >
</div> </div>
</div> <div>
<div class="float-right" mat-dialog-actions>
<button i18n mat-flat-button [mat-dialog-close]="undefined">Cancel</button>
<button <button
color="primary" color="primary"
i18n i18n
@ -36,3 +43,4 @@
Sign in Sign in
</button> </button>
</div> </div>
</div>

2
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.module.ts

@ -8,6 +8,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component'; import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component';
@NgModule({ @NgModule({
@ -16,6 +17,7 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
GfDialogHeaderModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,
MatDialogModule, MatDialogModule,

10
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.scss

@ -1,5 +1,15 @@
:host { :host {
display: block;
textarea.mat-input-element.cdk-textarea-autosize { textarea.mat-input-element.cdk-textarea-autosize {
box-sizing: content-box; box-sizing: content-box;
} }
.mat-checkbox {
::ng-deep {
label {
margin-bottom: 0;
}
}
}
} }

72
apps/client/src/app/pages/account/account-page.component.ts

@ -1,5 +1,14 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import {
import { MatDialog } from '@angular/material/dialog'; ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import {
MatSlideToggle,
MatSlideToggleChange
} from '@angular/material/slide-toggle';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
@ -7,8 +16,8 @@ import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'gf-account-page', selector: 'gf-account-page',
@ -16,6 +25,9 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./account-page.scss'] styleUrls: ['./account-page.scss']
}) })
export class AccountPageComponent implements OnDestroy, OnInit { export class AccountPageComponent implements OnDestroy, OnInit {
@ViewChild('toggleSignInWithFingerprintEnabledElement')
signInWithFingerprintElement: MatSlideToggle;
public accesses: Access[]; public accesses: Access[];
public baseCurrency: Currency; public baseCurrency: Currency;
public currencies: Currency[] = []; public currencies: Currency[] = [];
@ -30,7 +42,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
*/ */
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dialog: MatDialog,
private dataService: DataService, private dataService: DataService,
private userService: UserService, private userService: UserService,
public webAuthnService: WebAuthnService public webAuthnService: WebAuthnService
@ -88,21 +99,55 @@ export class AccountPageComponent implements OnDestroy, OnInit {
}); });
} }
public onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) {
if (aEvent.checked) {
this.registerDevice();
} else {
const confirmation = confirm(
'Do you really want to remove this sign in method?'
);
if (confirmation) {
this.deregisterDevice();
} else {
this.update();
}
}
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
public registerDevice() { private deregisterDevice() {
this.webAuthnService this.webAuthnService
.register() .deregister()
.subscribe(() => this.changeDetectorRef.markForCheck()); .pipe(
catchError(() => {
this.update();
return EMPTY;
})
)
.subscribe(() => {
this.update();
});
} }
public deregisterDevice() { private registerDevice() {
this.webAuthnService this.webAuthnService
.deregister() .register()
.subscribe(() => this.changeDetectorRef.markForCheck()); .pipe(
catchError(() => {
this.update();
return EMPTY;
})
)
.subscribe(() => {
this.update();
});
} }
private update() { private update() {
@ -112,6 +157,11 @@ export class AccountPageComponent implements OnDestroy, OnInit {
.subscribe((response) => { .subscribe((response) => {
this.accesses = response; this.accesses = response;
if (this.signInWithFingerprintElement) {
this.signInWithFingerprintElement.checked =
this.webAuthnService.isEnabled() ?? false;
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }

27
apps/client/src/app/pages/account/account-page.html

@ -66,29 +66,14 @@
</form> </form>
</div> </div>
</div> </div>
<div class="d-flex mt-4 py-1"> <div class="align-items-center d-flex mt-4 py-1">
<div class="pt-4 w-50" i18n>Authenticator</div> <div class="w-50" i18n>Sign in with fingerprint</div>
<div class="w-50"> <div class="w-50">
<button <mat-slide-toggle
*ngIf="!webAuthnService.isEnabled()" #toggleSignInWithFingerprintEnabledElement
class="d-inline-block"
color="primary"
i18n
mat-flat-button
(click)="registerDevice()"
>
Setup
</button>
<button
*ngIf="webAuthnService.isEnabled()"
class="d-inline-block"
color="primary" color="primary"
i18n (change)="onSignInWithFingerprintChange($event)"
mat-flat-button ></mat-slide-toggle>
(click)="deregisterDevice()"
>
Remove
</button>
</div> </div>
</div> </div>
</mat-card-content> </mat-card-content>

2
apps/client/src/app/pages/account/account-page.module.ts

@ -7,6 +7,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module'; import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { AccountPageRoutingModule } from './account-page-routing.module'; import { AccountPageRoutingModule } from './account-page-routing.module';
@ -26,6 +27,7 @@ import { AccountPageComponent } from './account-page.component';
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatSelectModule, MatSelectModule,
MatSlideToggleModule,
ReactiveFormsModule ReactiveFormsModule
], ],
providers: [] providers: []

8
apps/client/src/app/services/web-authn.service.ts

@ -36,6 +36,10 @@ export class WebAuthnService {
{} {}
) )
.pipe( .pipe(
catchError((error) => {
console.warn('Could not register device', error);
return of(null);
}),
switchMap((attOps) => { switchMap((attOps) => {
return startAttestation(attOps); return startAttestation(attOps);
}), }),
@ -59,8 +63,8 @@ export class WebAuthnService {
public deregister() { public deregister() {
const deviceId = this.getDeviceId(); const deviceId = this.getDeviceId();
return this.http.delete<AuthDeviceDto>(`/api/auth-device/${deviceId}`).pipe( return this.http.delete<AuthDeviceDto>(`/api/auth-device/${deviceId}`).pipe(
catchError((e) => { catchError((error) => {
console.warn(`Could not deregister device ${deviceId}`, e); console.warn(`Could not deregister device ${deviceId}`, error);
return of(null); return of(null);
}), }),
tap(() => tap(() =>

Loading…
Cancel
Save