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/), 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). 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 ## 1.15.0 - 14.06.2021
### Added ### Added

View File

@ -86,7 +86,6 @@
"main": "apps/client/src/main.ts", "main": "apps/client/src/main.ts",
"polyfills": "apps/client/src/polyfills.ts", "polyfills": "apps/client/src/polyfills.ts",
"tsConfig": "apps/client/tsconfig.app.json", "tsConfig": "apps/client/tsconfig.app.json",
"aot": true,
"assets": [ "assets": [
"apps/client/src/assets", "apps/client/src/assets",
{ {
@ -121,7 +120,13 @@
} }
], ],
"styles": ["apps/client/src/styles.scss"], "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": { "configurations": {
"production": { "production": {
@ -152,7 +157,8 @@
] ]
} }
}, },
"outputs": ["{options.outputPath}"] "outputs": ["{options.outputPath}"],
"defaultConfiguration": ""
}, },
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",

View File

@ -57,8 +57,9 @@ export class WebAuthService {
timeout: 60000, timeout: 60000,
attestationType: 'indirect', attestationType: 'indirect',
authenticatorSelection: { authenticatorSelection: {
userVerification: 'preferred', authenticatorAttachment: 'platform',
requireResidentKey: false requireResidentKey: false,
userVerification: 'required'
} }
}; };
@ -143,7 +144,7 @@ export class WebAuthService {
{ {
id: device.credentialId, id: device.credentialId,
type: 'public-key', type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal'] transports: ['internal']
} }
], ],
userVerification: 'preferred', userVerification: 'preferred',

View File

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

View File

@ -92,6 +92,13 @@ const routes: Routes = [
(m) => m.TransactionsPageModule (m) => m.TransactionsPageModule
) )
}, },
{
path: 'webauthn',
loadChildren: () =>
import('./pages/webauthn/webauthn-page.module').then(
(m) => m.WebauthnPageModule
)
},
{ {
path: 'zen', path: 'zen',
loadChildren: () => 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 { 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 { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.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 { 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 { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
@ -43,8 +46,8 @@ export class HeaderComponent implements OnChanges {
private dialog: MatDialog, private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService, private settingsStorageService: SettingsStorageService,
private webAuthnService: WebAuthnService private tokenStorageService: TokenStorageService
) { ) {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
@ -108,14 +111,17 @@ export class HeaderComponent implements OnChanges {
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
.subscribe(({ authToken }) => { .subscribe(({ authToken }) => {
this.setToken(authToken, data.staySignedIn); this.setToken(authToken);
}); });
} }
}); });
} }
public setToken(aToken: string, staySignedIn: boolean) { public setToken(aToken: string) {
this.tokenStorageService.saveToken(aToken, staySignedIn); this.tokenStorageService.saveToken(
aToken,
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
);
this.router.navigate(['/']); this.router.navigate(['/']);
} }

View File

@ -1,5 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {
STAY_SIGNED_IN,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
@Component({ @Component({
selector: 'gf-login-with-access-token-dialog', selector: 'gf-login-with-access-token-dialog',
@ -9,13 +14,21 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
}) })
export class LoginWithAccessTokenDialog { export class LoginWithAccessTokenDialog {
public constructor( public constructor(
@Inject(MAT_DIALOG_DATA) public data: any,
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>, public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
@Inject(MAT_DIALOG_DATA) public data: any private settingsStorageService: SettingsStorageService
) {} ) {}
ngOnInit() {} ngOnInit() {}
public onClose(): void { public onChangeStaySignedIn(aValue: MatCheckboxChange) {
this.settingsStorageService.setSetting(
STAY_SIGNED_IN,
aValue.checked?.toString()
);
}
public onClose() {
this.dialogRef.close(); this.dialogRef.close();
} }
} }

View File

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

View File

@ -79,10 +79,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
} }
} else if (error.status === StatusCodes.UNAUTHORIZED) { } else if (error.status === StatusCodes.UNAUTHORIZED) {
if (this.webAuthnService.isEnabled()) { if (this.webAuthnService.isEnabled()) {
this.webAuthnService.login().subscribe(({ authToken }) => { this.router.navigate(['/webauthn']);
this.tokenStorageService.saveToken(authToken, false);
window.location.reload();
});
} else { } else {
this.tokenStorageService.signOut(); this.tokenStorageService.signOut();
} }

View File

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

View File

@ -1,5 +1,9 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; 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'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
@Component({ @Component({
@ -14,6 +18,7 @@ export class AuthPageComponent implements OnInit {
public constructor( public constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService
) {} ) {}
@ -23,7 +28,10 @@ export class AuthPageComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.route.params.subscribe((params) => { this.route.params.subscribe((params) => {
const jwt = params['jwt']; const jwt = params['jwt'];
this.tokenStorageService.saveToken(jwt); this.tokenStorageService.saveToken(
jwt,
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
);
this.router.navigate(['/']); this.router.navigate(['/']);
}); });

View File

@ -26,8 +26,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService, private tokenStorageService: TokenStorageService
private webAuthnService: WebAuthnService
) {} ) {}
/** /**
@ -257,7 +256,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
} }
public setToken(aToken: string) { public setToken(aToken: string) {
this.tokenStorageService.saveToken(aToken); this.tokenStorageService.saveToken(aToken, true);
this.router.navigate(['/']); this.router.navigate(['/']);
} }

View File

@ -78,19 +78,13 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
dialogRef.afterClosed().subscribe((data) => { dialogRef.afterClosed().subscribe((data) => {
if (data?.authToken) { if (data?.authToken) {
this.tokenStorageService.saveToken(authToken); this.tokenStorageService.saveToken(authToken, true);
this.router.navigate(['/']); this.router.navigate(['/']);
} }
}); });
} }
public setToken(aToken: string) {
this.tokenStorageService.saveToken(aToken);
this.router.navigate(['/']);
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); 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'; import { Injectable } from '@angular/core';
export const RANGE = 'range'; export const RANGE = 'range';
export const STAY_SIGNED_IN = 'staySignedIn';
@Injectable({ @Injectable({
providedIn: 'root' 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) { if (staySignedIn) {
window.localStorage.setItem(TOKEN_KEY, token); 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 * This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown. * 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. * 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 * 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', displayName: 'common',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
globals: { globals: {
'ts-jest': { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' }
tsConfig: '<rootDir>/tsconfig.spec.json'
}
}, },
transform: { transform: {
'^.+\\.[tj]sx?$': 'ts-jest' '^.+\\.[tj]sx?$': 'ts-jest'

25
nx.json
View File

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

View File

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

4302
yarn.lock

File diff suppressed because it is too large Load Diff