From 0d6fe4a2321fdd07d74e4d5862fa06f0c9aba674 Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Mon, 24 May 2021 09:38:44 +0200 Subject: [PATCH] Feature/refactor user service as observable store (#117) * Implement user service as observable store * Clean up tokenStorageService usage * Update changelog --- CHANGELOG.md | 6 ++ apps/client/src/app/app.component.html | 1 + apps/client/src/app/app.component.ts | 40 +++++++------ .../app/components/header/header.component.ts | 9 ++- apps/client/src/app/core/auth.guard.ts | 8 ++- .../app/pages/about/about-page.component.ts | 26 ++++----- .../pages/account/account-page.component.ts | 28 ++++++---- .../pages/accounts/accounts-page.component.ts | 20 +++---- .../app/pages/admin/admin-page.component.ts | 15 +++-- .../pages/analysis/analysis-page.component.ts | 15 +++-- .../src/app/pages/home/home-page.component.ts | 20 +++---- .../pages/pricing/pricing-page.component.ts | 26 ++++----- .../transactions-page.component.ts | 20 +++---- .../src/app/pages/zen/zen-page.component.ts | 17 +++--- apps/client/src/app/services/data.service.ts | 11 ---- .../src/app/services/token-storage.service.ts | 11 ---- .../app/services/user/user-store.actions.ts | 4 ++ .../src/app/services/user/user-store.state.ts | 5 ++ .../src/app/services/user/user.service.ts | 56 +++++++++++++++++++ package.json | 1 + yarn.lock | 5 ++ 21 files changed, 199 insertions(+), 145 deletions(-) create mode 100644 apps/client/src/app/services/user/user-store.actions.ts create mode 100644 apps/client/src/app/services/user/user-store.state.ts create mode 100644 apps/client/src/app/services/user/user.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f378cd32..ccfc7897 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 + +- Introduced a user service implemented as an observable store (single source of truth for state) + ## 1.7.0 - 22.05.2021 ### Changed diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html index 75c8d49c..33245fb5 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 039277d1..f7d5ccff 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', @@ -30,7 +31,6 @@ export class AppComponent implements OnDestroy, OnInit { public currentYear = new Date().getFullYear(); public deviceType: string; public info: InfoItem; - public isLoggedIn = false; public user: User; public version = environment.version; @@ -42,7 +42,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; @@ -64,26 +65,22 @@ export class AppComponent implements OnDestroy, OnInit { this.currentRoute = urlSegments[0].path; }); - this.tokenStorageService - .onChangeHasToken() + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.isLoggedIn = !!this.tokenStorageService.getToken(); + .subscribe((state) => { + if (state?.user) { + this.user = state.user; - if (this.isLoggedIn) { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; - - this.canCreateAccount = hasPermission( - this.user.permissions, - permissions.createUserAccount - ); - - this.cd.markForCheck(); - }); - } else { + this.canCreateAccount = hasPermission( + this.user.permissions, + permissions.createUserAccount + ); + } else if (!this.tokenStorageService.getToken()) { + // User has not been logged in this.user = null; } + + this.cd.markForCheck(); }); } @@ -92,6 +89,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 e6113789..c8936c0f 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 d2e1fd76..12e94856 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 78cb264a..0d9db45a 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,5 @@ 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'; @@ -27,27 +26,22 @@ export class AboutPageComponent implements OnInit { */ public constructor( private cd: ChangeDetectorRef, - private dataService: DataService, - private tokenStorageService: TokenStorageService + private userService: UserService ) {} /** * Initializes the controller */ public ngOnInit() { - this.isLoggedIn = !!this.tokenStorageService.getToken(); + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; - if (this.isLoggedIn) - this.tokenStorageService - .onChangeHasToken() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; - - this.cd.markForCheck(); - }); - }); + this.cd.markForCheck(); + } + }); } public ngOnDestroy() { 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 c57eacde..fb25010c 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,6 @@ 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 +30,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { public constructor( private cd: ChangeDetectorRef, private dataService: DataService, - private tokenStorageService: TokenStorageService + private userService: UserService ) { this.dataService .fetchInfo() @@ -44,12 +44,11 @@ export class AccountPageComponent implements OnDestroy, OnInit { ); }); - this.tokenStorageService - .onChangeHasToken() + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; + .subscribe((state) => { + if (state?.user) { + this.user = state.user; this.hasPermissionToUpdateUserSettings = hasPermission( this.user.permissions, @@ -57,7 +56,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { ); this.cd.markForCheck(); - }); + } }); } @@ -78,11 +77,16 @@ export class AccountPageComponent implements OnDestroy, OnInit { }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; + this.userService.remove(); - this.cd.markForCheck(); - }); + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .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 ad4604e2..b86e8599 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -5,7 +5,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto 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 +42,7 @@ export class AccountsPageComponent implements OnInit { private impersonationStorageService: ImpersonationStorageService, private route: ActivatedRoute, private router: Router, - private tokenStorageService: TokenStorageService + private userService: UserService ) { this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) @@ -75,23 +75,23 @@ export class AccountsPageComponent implements OnInit { this.hasImpersonationId = !!aId; }); - this.tokenStorageService - .onChangeHasToken() + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + this.hasPermissionToCreateAccount = hasPermission( - user.permissions, + this.user.permissions, permissions.createAccount ); this.hasPermissionToDeleteAccount = hasPermission( - user.permissions, + this.user.permissions, permissions.deleteAccount ); this.cd.markForCheck(); - }); + } }); this.fetchAccounts(); 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 ade728f0..c715983d 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; 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 +34,7 @@ export class AdminPageComponent implements OnInit { private cacheService: CacheService, private cd: ChangeDetectorRef, private dataService: DataService, - private tokenStorageService: TokenStorageService + private userService: UserService ) {} /** @@ -43,13 +43,12 @@ export class AdminPageComponent implements OnInit { public ngOnInit() { this.fetchAdminData(); - this.tokenStorageService - .onChangeHasToken() + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; - }); + .subscribe((state) => { + if (state?.user) { + this.user = state.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 bd704ecd..7d68b6f1 100644 --- a/apps/client/src/app/pages/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/analysis/analysis-page.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type'; 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 +44,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private impersonationStorageService: ImpersonationStorageService, - private tokenStorageService: TokenStorageService + private userService: UserService ) {} /** @@ -79,15 +79,14 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { this.cd.markForCheck(); }); - this.tokenStorageService - .onChangeHasToken() + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; + .subscribe((state) => { + if (state?.user) { + this.user = state.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 515d1e63..df1f1397 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -10,7 +10,7 @@ import { RANGE, 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 +66,7 @@ export class HomePageComponent implements OnDestroy, OnInit { private route: ActivatedRoute, private router: Router, private settingsStorageService: SettingsStorageService, - private tokenStorageService: TokenStorageService + private userService: UserService ) { this.routeQueryParams = this.route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) @@ -76,14 +76,14 @@ export class HomePageComponent implements OnDestroy, OnInit { } }); - this.tokenStorageService - .onChangeHasToken() + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + this.hasPermissionToAccessFearAndGreedIndex = hasPermission( - user.permissions, + this.user.permissions, permissions.accessFearAndGreedIndex ); @@ -99,12 +99,12 @@ export class HomePageComponent implements OnDestroy, OnInit { } this.hasPermissionToReadForeignPortfolio = hasPermission( - user.permissions, + this.user.permissions, permissions.readForeignPortfolio ); this.cd.markForCheck(); - }); + } }); } 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 18915d26..650baa89 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,5 @@ 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'; @@ -23,27 +22,22 @@ export class PricingPageComponent implements OnInit { */ public constructor( private cd: ChangeDetectorRef, - private dataService: DataService, - private tokenStorageService: TokenStorageService + private userService: UserService ) {} /** * Initializes the controller */ public ngOnInit() { - this.isLoggedIn = !!this.tokenStorageService.getToken(); + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; - if (this.isLoggedIn) - this.tokenStorageService - .onChangeHasToken() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; - - this.cd.markForCheck(); - }); - }); + this.cd.markForCheck(); + } + }); } public ngOnDestroy() { 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 369629ac..ba38adfa 100644 --- a/apps/client/src/app/pages/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/transactions/transactions-page.component.ts @@ -5,7 +5,7 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; 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 +42,7 @@ export class TransactionsPageComponent implements OnInit { private impersonationStorageService: ImpersonationStorageService, private route: ActivatedRoute, private router: Router, - private tokenStorageService: TokenStorageService + private userService: UserService ) { this.routeQueryParams = route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) @@ -75,23 +75,23 @@ export class TransactionsPageComponent implements OnInit { this.hasImpersonationId = !!aId; }); - this.tokenStorageService - .onChangeHasToken() + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + this.hasPermissionToCreateOrder = hasPermission( - user.permissions, + this.user.permissions, permissions.createOrder ); this.hasPermissionToDeleteOrder = hasPermission( - user.permissions, + this.user.permissions, permissions.deleteOrder ); this.cd.markForCheck(); - }); + } }); this.fetchOrders(); 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 1fc5e65e..83596f7f 100644 --- a/apps/client/src/app/pages/zen/zen-page.component.ts +++ b/apps/client/src/app/pages/zen/zen-page.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface'; 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,22 +35,21 @@ export class ZenPageComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private impersonationStorageService: ImpersonationStorageService, - private tokenStorageService: TokenStorageService + private userService: UserService ) { - this.tokenStorageService - .onChangeHasToken() + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.dataService.fetchUser().subscribe((user) => { - this.user = user; + .subscribe((state) => { + if (state?.user) { + this.user = state.user; this.hasPermissionToReadForeignPortfolio = hasPermission( - user.permissions, + this.user.permissions, permissions.readForeignPortfolio ); this.cd.markForCheck(); - }); + } }); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 74fe3bc9..bf41116d 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/token-storage.service.ts b/apps/client/src/app/services/token-storage.service.ts index 88fcbe22..9e7e4751 100644 --- a/apps/client/src/app/services/token-storage.service.ts +++ b/apps/client/src/app/services/token-storage.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; const TOKEN_KEY = 'auth-token'; @@ -7,23 +6,15 @@ const TOKEN_KEY = 'auth-token'; providedIn: 'root' }) export class TokenStorageService { - private hasTokenChangeSubject = new BehaviorSubject(null); - public constructor() {} public getToken(): string { return window.localStorage.getItem(TOKEN_KEY); } - public onChangeHasToken() { - return this.hasTokenChangeSubject.asObservable(); - } - public saveToken(token: string): void { window.localStorage.removeItem(TOKEN_KEY); window.localStorage.setItem(TOKEN_KEY, token); - - this.hasTokenChangeSubject.next(); } public signOut(): void { @@ -34,7 +25,5 @@ export class TokenStorageService { if (utmSource) { window.localStorage.setItem('utm_source', utmSource); } - - this.hasTokenChangeSubject.next(); } } 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 00000000..cbacd70d --- /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 00000000..31e397a4 --- /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 00000000..0d2df2b6 --- /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: undefined }, '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 fe753ad8..9f4228f4 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 a7d688a6..f4becfbf 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"