diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html index 75c8d49c4..33245fb54 100644 --- a/apps/client/src/app/app.component.html +++ b/apps/client/src/app/app.component.html @@ -4,6 +4,7 @@ [currentRoute]="currentRoute" [info]="info" [user]="user" + (signOut)="onSignOut()" > diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 039277d17..738d62a92 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -17,6 +17,7 @@ import { filter, takeUntil } from 'rxjs/operators'; import { environment } from '../environments/environment'; import { DataService } from './services/data.service'; import { TokenStorageService } from './services/token-storage.service'; +import { UserService } from './services/user/user.service'; @Component({ selector: 'gf-root', @@ -42,7 +43,8 @@ export class AppComponent implements OnDestroy, OnInit { private deviceService: DeviceDetectorService, private materialCssVarsService: MaterialCssVarsService, private router: Router, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) { this.initializeTheme(); this.user = undefined; @@ -71,7 +73,7 @@ export class AppComponent implements OnDestroy, OnInit { this.isLoggedIn = !!this.tokenStorageService.getToken(); if (this.isLoggedIn) { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; this.canCreateAccount = hasPermission( @@ -92,6 +94,13 @@ export class AppComponent implements OnDestroy, OnInit { window.location.reload(); } + public onSignOut() { + this.tokenStorageService.signOut(); + this.userService.remove(); + + window.location.reload(); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index e6113789e..c8936c0fd 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -1,8 +1,10 @@ import { ChangeDetectionStrategy, Component, + EventEmitter, Input, - OnChanges + OnChanges, + Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; @@ -26,6 +28,8 @@ export class HeaderComponent implements OnChanges { @Input() info: InfoItem; @Input() user: User; + @Output() signOut = new EventEmitter(); + public hasPermissionForSocialLogin: boolean; public hasPermissionForSubscription: boolean; public hasPermissionToAccessAdminControl: boolean; @@ -75,8 +79,7 @@ export class HeaderComponent implements OnChanges { } public onSignOut() { - this.tokenStorageService.signOut(); - window.location.reload(); + this.signOut.next(); } public openLoginDialog(): void { diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index d2e1fd768..12e948566 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -11,13 +11,15 @@ import { catchError } from 'rxjs/operators'; import { DataService } from '../services/data.service'; import { SettingsStorageService } from '../services/settings-storage.service'; +import { UserService } from '../services/user/user.service'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( private dataService: DataService, private router: Router, - private settingsStorageService: SettingsStorageService + private settingsStorageService: SettingsStorageService, + private userService: UserService ) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { @@ -29,8 +31,8 @@ export class AuthGuard implements CanActivate { } return new Promise((resolve) => { - this.dataService - .fetchUser() + this.userService + .get() .pipe( catchError(() => { if (state.url !== '/start') { diff --git a/apps/client/src/app/pages/about/about-page.component.ts b/apps/client/src/app/pages/about/about-page.component.ts index 78cb264ae..28ec58a9f 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { baseCurrency } from '@ghostfolio/common/config'; import { User } from '@ghostfolio/common/interfaces'; import { Subject } from 'rxjs'; @@ -28,7 +29,8 @@ export class AboutPageComponent implements OnInit { public constructor( private cd: ChangeDetectorRef, private dataService: DataService, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) {} /** @@ -42,7 +44,7 @@ export class AboutPageComponent implements OnInit { .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; this.cd.markForCheck(); diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts index c57eacde7..be049bddf 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { Access, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -30,7 +31,8 @@ export class AccountPageComponent implements OnDestroy, OnInit { public constructor( private cd: ChangeDetectorRef, private dataService: DataService, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) { this.dataService .fetchInfo() @@ -48,7 +50,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; this.hasPermissionToUpdateUserSettings = hasPermission( @@ -78,7 +80,9 @@ export class AccountPageComponent implements OnDestroy, OnInit { }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.remove(); + + this.userService.get().subscribe((user) => { this.user = user; this.cd.markForCheck(); diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index ad4604e24..d6fb22bbc 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -6,6 +6,7 @@ import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Account as AccountModel, AccountType } from '@prisma/client'; @@ -42,7 +43,8 @@ export class AccountsPageComponent implements OnInit { private impersonationStorageService: ImpersonationStorageService, private route: ActivatedRoute, private router: Router, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) { this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) @@ -79,8 +81,9 @@ export class AccountsPageComponent implements OnInit { .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; + this.hasPermissionToCreateAccount = hasPermission( user.permissions, permissions.createAccount diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index ade728f03..e69cb4d9d 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -3,6 +3,7 @@ import { AdminService } from '@ghostfolio/client/services/admin.service'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { AdminData, User } from '@ghostfolio/common/interfaces'; import { formatDistanceToNow, isValid, parseISO, sub } from 'date-fns'; @@ -34,7 +35,8 @@ export class AdminPageComponent implements OnInit { private cacheService: CacheService, private cd: ChangeDetectorRef, private dataService: DataService, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) {} /** @@ -47,7 +49,7 @@ export class AdminPageComponent implements OnInit { .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; }); }); diff --git a/apps/client/src/app/pages/analysis/analysis-page.component.ts b/apps/client/src/app/pages/analysis/analysis-page.component.ts index bd704ecd9..addb38884 100644 --- a/apps/client/src/app/pages/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/analysis/analysis-page.component.ts @@ -3,6 +3,7 @@ import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/to import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { PortfolioItem, PortfolioPosition, @@ -44,7 +45,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private impersonationStorageService: ImpersonationStorageService, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) {} /** @@ -83,7 +85,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; this.cd.markForCheck(); diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index 515d1e63b..1fe0f8236 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -11,6 +11,7 @@ import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { PortfolioOverview, PortfolioPerformance, @@ -66,7 +67,8 @@ export class HomePageComponent implements OnDestroy, OnInit { private route: ActivatedRoute, private router: Router, private settingsStorageService: SettingsStorageService, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) { this.routeQueryParams = this.route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) @@ -80,8 +82,9 @@ export class HomePageComponent implements OnDestroy, OnInit { .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; + this.hasPermissionToAccessFearAndGreedIndex = hasPermission( user.permissions, permissions.accessFearAndGreedIndex diff --git a/apps/client/src/app/pages/pricing/pricing-page.component.ts b/apps/client/src/app/pages/pricing/pricing-page.component.ts index 18915d265..e6d0ab5a2 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.component.ts +++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { baseCurrency } from '@ghostfolio/common/config'; import { User } from '@ghostfolio/common/interfaces'; import { Subject } from 'rxjs'; @@ -24,7 +25,8 @@ export class PricingPageComponent implements OnInit { public constructor( private cd: ChangeDetectorRef, private dataService: DataService, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) {} /** @@ -38,7 +40,7 @@ export class PricingPageComponent implements OnInit { .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; this.cd.markForCheck(); diff --git a/apps/client/src/app/pages/transactions/transactions-page.component.ts b/apps/client/src/app/pages/transactions/transactions-page.component.ts index 369629ac7..d1381417e 100644 --- a/apps/client/src/app/pages/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/transactions/transactions-page.component.ts @@ -6,6 +6,7 @@ import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Order as OrderModel } from '@prisma/client'; @@ -42,7 +43,8 @@ export class TransactionsPageComponent implements OnInit { private impersonationStorageService: ImpersonationStorageService, private route: ActivatedRoute, private router: Router, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) { this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) @@ -79,8 +81,9 @@ export class TransactionsPageComponent implements OnInit { .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; + this.hasPermissionToCreateOrder = hasPermission( user.permissions, permissions.createOrder diff --git a/apps/client/src/app/pages/zen/zen-page.component.ts b/apps/client/src/app/pages/zen/zen-page.component.ts index 1fc5e65ee..f30102117 100644 --- a/apps/client/src/app/pages/zen/zen-page.component.ts +++ b/apps/client/src/app/pages/zen/zen-page.component.ts @@ -3,6 +3,7 @@ import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfac import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; @@ -35,13 +36,14 @@ export class ZenPageComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private impersonationStorageService: ImpersonationStorageService, - private tokenStorageService: TokenStorageService + private tokenStorageService: TokenStorageService, + private userService: UserService ) { this.tokenStorageService .onChangeHasToken() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { + this.userService.get().subscribe((user) => { this.user = user; this.hasPermissionToReadForeignPortfolio = hasPermission( diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 74fe3bc9c..bf41116dd 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -74,13 +74,6 @@ export class DataService { } public fetchInfo() { - /* - if (this.info) { - // TODO: Cache info - return of(this.info); - } - */ - return this.http.get('/api/info').pipe( map((data) => { if ( @@ -154,10 +147,6 @@ export class DataService { ); } - public fetchUser() { - return this.http.get('/api/user'); - } - public loginAnonymous(accessToken: string) { return this.http.get(`/api/auth/anonymous/${accessToken}`); } diff --git a/apps/client/src/app/services/user/user-store.actions.ts b/apps/client/src/app/services/user/user-store.actions.ts new file mode 100644 index 000000000..cbacd70d0 --- /dev/null +++ b/apps/client/src/app/services/user/user-store.actions.ts @@ -0,0 +1,4 @@ +export enum UserStoreActions { + GetUser = 'GET_USER', + RemoveUser = 'REMOVE_USER' +} diff --git a/apps/client/src/app/services/user/user-store.state.ts b/apps/client/src/app/services/user/user-store.state.ts new file mode 100644 index 000000000..31e397a41 --- /dev/null +++ b/apps/client/src/app/services/user/user-store.state.ts @@ -0,0 +1,5 @@ +import { User } from '@ghostfolio/common/interfaces'; + +export interface UserStoreState { + user: User; +} diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts new file mode 100644 index 000000000..ba8a37bb4 --- /dev/null +++ b/apps/client/src/app/services/user/user.service.ts @@ -0,0 +1,56 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ObservableStore } from '@codewithdan/observable-store'; +import { User } from '@ghostfolio/common/interfaces'; +import { of } from 'rxjs'; +import { throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +import { UserStoreActions } from './user-store.actions'; +import { UserStoreState } from './user-store.state'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService extends ObservableStore { + public constructor(private http: HttpClient) { + super({ trackStateHistory: true }); + + this.setState({ user: null }, 'INIT_STATE'); + } + + public get() { + const state = this.getState(); + + if (state?.user) { + // Get from cache + return of(state.user); + } else { + // Get from endpoint + return this.fetchUser().pipe(catchError(this.handleError)); + } + } + + public remove() { + this.setState({ user: null }, UserStoreActions.RemoveUser); + } + + private fetchUser() { + return this.http.get('/api/user').pipe( + map((user) => { + this.setState({ user }, UserStoreActions.GetUser); + return user; + }), + catchError(this.handleError) + ); + } + + private handleError(error: any) { + if (error.error instanceof Error) { + const errMessage = error.error.message; + return throwError(errMessage); + } + + return throwError(error || 'Server error'); + } +} diff --git a/package.json b/package.json index fe753ad81..9f4228f45 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@angular/platform-browser": "11.2.4", "@angular/platform-browser-dynamic": "11.2.4", "@angular/router": "11.2.4", + "@codewithdan/observable-store": "2.2.11", "@nestjs/common": "7.6.5", "@nestjs/config": "0.6.1", "@nestjs/core": "7.6.5", diff --git a/yarn.lock b/yarn.lock index a7d688a64..f4becfbf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1396,6 +1396,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@codewithdan/observable-store@2.2.11": + version "2.2.11" + resolved "https://registry.yarnpkg.com/@codewithdan/observable-store/-/observable-store-2.2.11.tgz#f5a168e86a2fa185a50ca40a1e838aa5e5fb007d" + integrity sha512-6CfqLJUqV0SwS4yE+9vciqxHUJ6CqIptSXXzFw80MonCDoVJvCJ/xhKfs7VZqJ4jDtEu/7ILvovFtZdLg9fiAg== + "@ctrl/tinycolor@^2.6.0": version "2.6.1" resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-2.6.1.tgz#0e78cc836a1fd997a9a22fa1c26c555411882160"