diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 3a927d998..0b41aaa87 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -7,7 +7,7 @@ import { AudioService } from '../../services/audio.service'; import { ETA, EtaService } from '../../services/eta.service'; import { Transaction } from '../../interfaces/electrs.interface'; import { MiningStats } from '../../services/mining.service'; -import { StorageService } from '../../services/storage.service'; +import { IAuth, AuthServiceMempool } from '../../services/auth.service'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp'; @@ -71,7 +71,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { simpleMode: boolean = true; paymentMethod: 'cashapp' | 'btcpay'; - user: any = undefined; + authSubscription$: Subscription; + auth: IAuth | null = null; // accelerator stuff square: { appId: string, locationId: string}; @@ -109,16 +110,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy { constructor( public stateService: StateService, private servicesApiService: ServicesApiServices, - private storageService: StorageService, private etaService: EtaService, private audioService: AudioService, - private cd: ChangeDetectorRef + private cd: ChangeDetectorRef, + private authService: AuthServiceMempool ) { this.accelerationUUID = window.crypto.randomUUID(); } ngOnInit() { - this.user = this.storageService.getAuth()?.user ?? null; + this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => { + this.auth = auth; + this.estimate = null; + this.moveToStep('summary'); + }); + this.authService.refreshAuth$().subscribe(); + const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('cash_request_id')) { // Redirected from cashapp this.moveToStep('processing'); @@ -146,6 +153,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (this.estimateSubscription) { this.estimateSubscription.unsubscribe(); } + if (this.authSubscription$) { + this.authSubscription$.unsubscribe(); + } } ngOnChanges(changes: SimpleChanges): void { @@ -456,8 +466,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } isLoggedIn(): boolean { - const auth = this.storageService.getAuth(); - return auth !== null; + return this.auth !== null; } /** diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 591a8730e..25843bb28 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -129,7 +129,7 @@
- + diff --git a/frontend/src/app/components/menu/menu.component.ts b/frontend/src/app/components/menu/menu.component.ts index 7a2d8b6ed..719495bb0 100644 --- a/frontend/src/app/components/menu/menu.component.ts +++ b/frontend/src/app/components/menu/menu.component.ts @@ -5,6 +5,7 @@ import { StorageService } from '../../services/storage.service'; import { Router, NavigationStart } from '@angular/router'; import { StateService } from '../../services/state.service'; import { IUser, ServicesApiServices } from '../../services/services-api.service'; +import { AuthServiceMempool } from '../../services/auth.service'; @Component({ selector: 'app-menu', @@ -26,7 +27,8 @@ export class MenuComponent implements OnInit, OnDestroy { private servicesApiServices: ServicesApiServices, private storageService: StorageService, private router: Router, - private stateService: StateService + private stateService: StateService, + private authService: AuthServiceMempool ) {} ngOnInit(): void { @@ -61,12 +63,19 @@ export class MenuComponent implements OnInit, OnDestroy { this.loggedOut.emit(true); if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) { this.userMenuGroups$ = this.servicesApiServices.getUserMenuGroups$(); - this.router.navigateByUrl('/'); + this.authService.logout(); + if (window.location.toString().includes('services')) { + this.router.navigateByUrl('/login'); + } } }); } onLinkClick(link) { + if (link === 'logout') { + this.toggleMenu(false); + return; + } if (!this.isServicesPage || this.isSmallScreen()) { this.toggleMenu(false); } diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts new file mode 100644 index 000000000..7a4fcda24 --- /dev/null +++ b/frontend/src/app/services/auth.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { map, Observable, ReplaySubject, switchMap, tap } from 'rxjs'; +import { ServicesApiServices } from './services-api.service'; + +export interface IAuth { + token: string; + user: { + userId: number; + username: string; + } +} + +@Injectable({ + providedIn: 'root' +}) +export class AuthServiceMempool { + private auth$: ReplaySubject = new ReplaySubject(1); + + constructor( + private servicesApiService: ServicesApiServices, + private router: Router, + ) { + const localStorageAuth = localStorage.getItem('auth'); + if (!localStorageAuth || localStorageAuth.length === 0) { + this.setAuth(null); + } else { + try { + this.setAuth(JSON.parse(localStorageAuth)); + } catch (e) { + console.error(`Unable to parse 'auth' from localStorage`, e); + localStorage.removeItem('auth'); + this.setAuth(null); + } + } + } + + refreshAuth$(): Observable { + return this.servicesApiService.getJWT$() + .pipe( + tap((user) => { + this.setAuth(user); + }), + map((user) => { + return user; + }) + ); + } + + logout() { + this.setAuth(null); + } + + setAuth(auth: any) { + if (!auth) { + localStorage.removeItem('auth'); + } else { + localStorage.setItem('auth', JSON.stringify(auth)); + } + this.auth$.next(auth); + } + + getAuth$(): Observable { + if (!localStorage.getItem('auth')) { + return this.refreshAuth$().pipe( + switchMap(() => this.auth$.asObservable()) + ); + } + return this.auth$.asObservable(); + } +} diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 0dc58b957..07e72e0b6 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -1,6 +1,6 @@ import { Router, NavigationStart } from '@angular/router'; import { Injectable } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { StateService } from './state.service'; import { StorageService } from './storage.service'; import { MenuGroup } from '../interfaces/services.interface'; @@ -120,6 +120,10 @@ export class ServicesApiServices { return this.httpClient.post(`${SERVICES_API_PREFIX}/auth/logout`, {}); } + getJWT$() { + return this.httpClient.get(`${SERVICES_API_PREFIX}/auth/getJWT`); + } + getServicesBackendInfo$(): Observable { return this.httpClient.get(`${SERVICES_API_PREFIX}/version`); } diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index 69fdf50e7..bd1783513 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -33,8 +33,8 @@ } @if (!env.customize?.theme) { @@ -44,8 +44,8 @@ } @if (!enterpriseInfo?.footer_img) {

Explore the full Bitcoin ecosystem diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.ts b/frontend/src/app/shared/components/global-footer/global-footer.component.ts index 826172c4b..50f9a53d7 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.ts +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID, HostListener, OnDestroy } from '@angular/core'; +import { Input, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, OnChanges, SimpleChanges, Inject, LOCALE_ID, OnDestroy } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { Observable, merge, of, Subject, Subscription } from 'rxjs'; import { tap, takeUntil } from 'rxjs/operators'; @@ -16,7 +16,9 @@ import { EnterpriseService } from '../../../services/enterprise.service'; styleUrls: ['./global-footer.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class GlobalFooterComponent implements OnInit, OnDestroy { +export class GlobalFooterComponent implements OnInit, OnDestroy, OnChanges { + @Input() user: any = undefined; + private destroy$: Subject = new Subject(); env: Env; officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; @@ -29,7 +31,6 @@ export class GlobalFooterComponent implements OnInit, OnDestroy { network$: Observable; networkPaths: { [network: string]: string }; currentNetwork = ''; - loggedIn = false; urlSubscription: Subscription; isServicesPage = false; @@ -72,11 +73,17 @@ export class GlobalFooterComponent implements OnInit, OnDestroy { }); this.urlSubscription = this.route.url.subscribe((url) => { - this.loggedIn = this.storageService.getAuth() !== null; + this.user = this.storageService.getAuth(); this.cd.markForCheck(); }) } + ngOnChanges(changes: SimpleChanges): void { + if (changes.user) { + this.user = this.storageService.getAuth(); + } + } + ngOnDestroy(): void { this.destroy$.next(true); this.destroy$.complete();