Compare commits

..

9 Commits

Author SHA1 Message Date
198eaf57d3 Release 1.17.0 (#171) 2021-06-15 21:17:41 +02:00
6783ea2ebb Feature/upgrade various frontend dependencies (#170)
* Upgrade frontend dependencies

* Update changelog
2021-06-15 21:15:48 +02:00
a35701fe24 Feature/upgrade to angular 12 (#169)
* Upgrade to Angular 12

* Update changelog
2021-06-15 21:03:55 +02:00
5db90f1787 Feature/improve error page of fingerprint sign in (#167)
* Improve error page

* Update changelog
2021-06-15 09:47:18 +02:00
81fe538484 Order attribute 2021-06-15 09:43:48 +02:00
51884913be Feature/disable fingerprint sign in in demo account page (#163)
* Disable fingerprint toggle for demo user

* Update changelog
2021-06-15 09:21:53 +02:00
8886082dfa Feature/upgrade eslint and prettier dependencies (#164)
* Upgrade eslint and prettier dependencies

* Feature/upgrade date fns to version 2.22.1 (#165)

* Feature/upgrade chart.js to version 3.3.2 (#166)

* Update changelog
2021-06-15 09:17:27 +02:00
3b12e5b85b Release 1.16.0 (#162) 2021-06-14 22:00:00 +02:00
6c1119caec Restrict webauthn to fingerprint only and improve UX (#161)
* Restrict webauthn to fingerprint only

* Move webauthn login to separate page /webauthn

* Stay signed in with social login

* Update changelog

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2021-06-14 21:57:09 +02:00
27 changed files with 2498 additions and 2151 deletions

View File

@ -5,6 +5,26 @@ 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).
## 1.17.0 - 15.06.2021
### Changed
- Improved the error page of the sign in with fingerprint
- Disable the sign in with fingerprint selector for the demo user
- Upgraded `angular` from version `11.2.4` to `12.0.4`
- Upgraded `angular-material-css-vars` from version `1.1.2` to `1.2.0`
- Upgraded `chart.js` from version `3.2.1` to `3.3.2`
- Upgraded `date-fns` from version `2.19.0` to `2.22.1`
- Upgraded `eslint` and `prettier` dependencies
- Upgraded `ngx-device-detector` from version `2.0.6` to `2.1.1`
- Upgraded `ngx-markdown` from version `11.1.2` to `12.0.1`
## 1.16.0 - 14.06.2021
### Changed
- Improved the sign in with fingerprint
## 1.15.0 - 14.06.2021
### Added

View File

@ -86,7 +86,6 @@
"main": "apps/client/src/main.ts",
"polyfills": "apps/client/src/polyfills.ts",
"tsConfig": "apps/client/tsconfig.app.json",
"aot": true,
"assets": [
"apps/client/src/assets",
{
@ -121,7 +120,13 @@
}
],
"styles": ["apps/client/src/styles.scss"],
"scripts": ["node_modules/marked/lib/marked.js"]
"scripts": ["node_modules/marked/lib/marked.js"],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
@ -152,7 +157,8 @@
]
}
},
"outputs": ["{options.outputPath}"]
"outputs": ["{options.outputPath}"],
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",

View File

@ -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',

View File

@ -16,8 +16,8 @@ module.exports = {
},
coverageDirectory: '../../coverage/apps/client',
snapshotSerializers: [
'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
'jest-preset-angular/build/AngularSnapshotSerializer.js',
'jest-preset-angular/build/HTMLCommentSerializer.js'
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment'
]
};

View File

@ -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: () =>

View File

@ -11,8 +11,11 @@ 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';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { EMPTY, Subject } from 'rxjs';
@ -43,8 +46,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 +111,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(['/']);
}

View File

@ -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<LoginWithAccessTokenDialog>,
@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();
}
}

View File

@ -28,7 +28,7 @@
</div>
<div mat-dialog-actions>
<div class="flex-grow-1">
<mat-checkbox i18n [(ngModel)]="data.staySignedIn"
<mat-checkbox i18n (change)="onChangeStaySignedIn($event)"
>Stay signed in</mat-checkbox
>
</div>

View File

@ -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();
}

View File

@ -72,6 +72,7 @@
<mat-slide-toggle
#toggleSignInWithFingerprintEnabledElement
color="primary"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onSignInWithFingerprintChange($event)"
></mat-slide-toggle>
</div>

View File

@ -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(['/']);
});

View File

@ -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(['/']);
}

View File

@ -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();

View File

@ -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 {}

View File

@ -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();
}
);
}
}

View File

@ -0,0 +1,34 @@
<div class="container">
<div class="row">
<div
class="align-items-center d-flex flex-column justify-content-center mb-4 w-100"
>
<gf-logo size="medium"></gf-logo>
</div>
<div *ngIf="!hasError" class="col d-flex justify-content-center">
<mat-spinner [diameter]="20"></mat-spinner>
</div>
<div
*ngIf="hasError"
class="align-items-center col d-flex flex-column justify-content-center"
>
<h1 class="d-flex h5 justify-content-center mb-0 text-center" i18n>
Oops, authentication has failed.
</h1>
<button
class="mb-3 mt-4"
color="primary"
i18n
mat-flat-button
(click)="signIn()"
>
Try again
</button>
<div class="text-muted" i18n>or</div>
<button class="mt-1" i18n mat-flat-button (click)="deregisterDevice()">
Go back to Home Page
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
@NgModule({
declarations: [WebauthnPageComponent],
exports: [],
imports: [
CommonModule,
GfLogoModule,
MatButtonModule,
MatProgressSpinnerModule,
WebauthnPageRoutingModule
],
providers: []
})
export class WebauthnPageModule {}

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
export const RANGE = 'range';
export const STAY_SIGNED_IN = 'staySignedIn';
@Injectable({
providedIn: 'root'

View File

@ -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);
}

View File

@ -15,4 +15,4 @@ export const environment = {
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

View File

@ -55,7 +55,7 @@
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS

View File

@ -1 +1 @@
import 'jest-preset-angular';
import 'jest-preset-angular/setup-jest';

View File

@ -2,9 +2,7 @@ module.exports = {
displayName: 'common',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json'
}
'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' }
},
transform: {
'^.+\\.[tj]sx?$': 'ts-jest'

25
nx.json
View File

@ -1,26 +1,39 @@
{
"implicitDependencies": {
"angular.json": "*",
"package.json": { "dependencies": "*", "devDependencies": "*" },
"package.json": {
"dependencies": "*",
"devDependencies": "*"
},
"tsconfig.base.json": "*",
".eslintrc.json": "*",
"nx.json": "*"
},
"affected": { "defaultBase": "origin/main" },
"affected": {
"defaultBase": "origin/main"
},
"npmScope": "ghostfolio",
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/workspace/tasks-runners/default",
"options": { "cacheableOperations": ["build", "lint", "test", "e2e"] }
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"]
}
}
},
"projects": {
"api": { "tags": [] },
"client": { "tags": [] },
"api": {
"tags": []
},
"client": {
"tags": []
},
"client-e2e": {
"tags": [],
"implicitDependencies": ["client"]
},
"common": { "tags": [] }
"common": {
"tags": []
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.15.0",
"version": "1.17.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -45,16 +45,16 @@
"workspace-generator": "nx workspace-generator"
},
"dependencies": {
"@angular/animations": "11.2.4",
"@angular/animations": "12.0.4",
"@angular/cdk": "11.0.4",
"@angular/common": "11.2.4",
"@angular/compiler": "11.2.4",
"@angular/core": "11.2.4",
"@angular/forms": "11.2.4",
"@angular/common": "12.0.4",
"@angular/compiler": "12.0.4",
"@angular/core": "12.0.4",
"@angular/forms": "12.0.4",
"@angular/material": "11.0.4",
"@angular/platform-browser": "11.2.4",
"@angular/platform-browser-dynamic": "11.2.4",
"@angular/router": "11.2.4",
"@angular/platform-browser": "12.0.4",
"@angular/platform-browser-dynamic": "12.0.4",
"@angular/router": "12.0.4",
"@codewithdan/observable-store": "2.2.11",
"@nestjs/common": "7.6.5",
"@nestjs/config": "0.6.1",
@ -64,33 +64,33 @@
"@nestjs/platform-express": "7.6.5",
"@nestjs/schedule": "0.4.1",
"@nestjs/serve-static": "2.1.4",
"@nrwl/angular": "12.0.0",
"@nrwl/angular": "12.3.6",
"@prisma/client": "2.24.1",
"@simplewebauthn/browser": "3.0.0",
"@simplewebauthn/server": "3.0.0",
"@simplewebauthn/typescript-types": "3.0.0",
"@types/lodash": "4.14.168",
"alphavantage": "2.2.0",
"angular-material-css-vars": "1.1.2",
"angular-material-css-vars": "1.2.0",
"bent": "7.3.12",
"bootstrap": "4.6.0",
"cache-manager": "3.4.3",
"cache-manager-redis-store": "2.0.0",
"chart.js": "3.2.1",
"chartjs-adapter-date-fns": "1.1.0-beta.1",
"chart.js": "3.3.2",
"chartjs-adapter-date-fns": "2.0.0",
"cheerio": "1.0.0-rc.6",
"class-transformer": "0.3.2",
"class-validator": "0.13.1",
"countries-list": "2.6.1",
"countup.js": "2.0.7",
"cryptocurrencies": "7.0.0",
"date-fns": "2.19.0",
"date-fns": "2.22.1",
"envalid": "7.1.0",
"http-status-codes": "2.1.4",
"ionicons": "5.5.1",
"lodash": "4.17.21",
"ngx-device-detector": "2.0.6",
"ngx-markdown": "11.1.2",
"ngx-device-detector": "2.1.1",
"ngx-markdown": "12.0.1",
"ngx-skeleton-loader": "2.9.1",
"passport": "0.4.1",
"passport-google-oauth20": "2.0.0",
@ -102,47 +102,47 @@
"svgmap": "2.1.1",
"uuid": "8.3.2",
"yahoo-finance": "0.3.6",
"zone.js": "0.11.4"
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "0.1102.3",
"@angular-eslint/eslint-plugin": "2.0.2",
"@angular/cli": "11.2.3",
"@angular/compiler-cli": "11.2.4",
"@angular/language-service": "11.2.4",
"@angular-devkit/build-angular": "12.0.4",
"@angular-eslint/eslint-plugin": "12.0.0",
"@angular/cli": "12.0.4",
"@angular/compiler-cli": "12.0.4",
"@angular/language-service": "12.0.4",
"@angular/localize": "11.0.9",
"@nestjs/schematics": "7.2.6",
"@nestjs/testing": "7.6.5",
"@nrwl/cli": "12.0.0",
"@nrwl/cypress": "12.0.0",
"@nrwl/eslint-plugin-nx": "12.0.0",
"@nrwl/jest": "12.0.0",
"@nrwl/nest": "12.0.0",
"@nrwl/node": "12.0.0",
"@nrwl/tao": "12.0.0",
"@nrwl/workspace": "12.0.0",
"@nrwl/cli": "12.3.6",
"@nrwl/cypress": "12.3.6",
"@nrwl/eslint-plugin-nx": "12.3.6",
"@nrwl/jest": "12.3.6",
"@nrwl/nest": "12.3.6",
"@nrwl/node": "12.3.6",
"@nrwl/tao": "12.3.6",
"@nrwl/workspace": "12.3.6",
"@types/cache-manager": "3.4.0",
"@types/jest": "26.0.20",
"@types/node": "14.14.33",
"@types/passport-google-oauth20": "2.0.6",
"@typescript-eslint/eslint-plugin": "4.19.0",
"@typescript-eslint/parser": "4.19.0",
"@typescript-eslint/eslint-plugin": "4.27.0",
"@typescript-eslint/parser": "4.27.0",
"codelyzer": "6.0.1",
"cypress": "6.2.1",
"eslint": "7.22.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-import": "latest",
"eslint": "7.28.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-import": "2.23.4",
"import-sort-cli": "6.0.0",
"import-sort-parser-typescript": "6.0.0",
"import-sort-style-module": "6.0.0",
"jest": "26.6.3",
"jest-preset-angular": "8.3.2",
"prettier": "2.2.1",
"jest-preset-angular": "8.4.0",
"prettier": "2.3.1",
"replace-in-file": "6.2.0",
"rimraf": "3.0.2",
"ts-jest": "26.4.4",
"ts-jest": "26.5.5",
"ts-node": "9.1.1",
"typescript": "4.1.4"
"typescript": "4.2.4"
},
"engines": {
"node": "14.x"

4302
yarn.lock

File diff suppressed because it is too large Load Diff