diff --git a/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.html b/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.html index 6b84bbc8..3451137c 100644 --- a/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.html +++ b/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.html @@ -5,7 +5,7 @@ diff --git a/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.ts b/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.ts index ae07aab4..ab735bab 100644 --- a/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.ts +++ b/frontend/src/app/components/medical-sources-connected/medical-sources-connected.component.ts @@ -10,6 +10,7 @@ import {ToastNotification, ToastType} from '../../models/fasten/toast'; import {ToastService} from '../../services/toast.service'; import {ActivatedRoute, Router} from '@angular/router'; import {Location} from '@angular/common'; +import {EventBusService} from '../../services/event-bus.service'; @Component({ selector: 'app-medical-sources-connected', @@ -33,6 +34,7 @@ export class MedicalSourcesConnectedComponent implements OnInit { private activatedRoute: ActivatedRoute, private router: Router, private location: Location, + private eventBusService: EventBusService, ) { } ngOnInit(): void { @@ -64,6 +66,9 @@ export class MedicalSourcesConnectedComponent implements OnInit { .then(console.log) } + this.eventBusService.eventBusSourceSyncMessages.subscribe((event) => { + this.status[event.source_id] = "token" + }) } @@ -171,6 +176,7 @@ export class MedicalSourcesConnectedComponent implements OnInit { .subscribe((resp) => { // const sourceSyncMessage = JSON.parse(msg) as SourceSyncMessage delete this.status[sourceType] + delete this.status[resp.source.id] // window.location.reload(); // this.connectedSourceList. @@ -269,7 +275,7 @@ export class MedicalSourcesConnectedComponent implements OnInit { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public openModal(contentModalRef, sourceListItem: SourceListItem) { - if(this.status[sourceListItem.metadata.source_type] || !sourceListItem.source){ + if(this.status[sourceListItem.metadata.source_type] || !sourceListItem.source || this.status[sourceListItem.source.id]){ //if this source is currently "loading" dont open the modal window return } @@ -287,15 +293,17 @@ export class MedicalSourcesConnectedComponent implements OnInit { public sourceSyncHandler(source: Source){ - this.status[source.source_type] = "authorize" + this.status[source.id] = "authorize" this.modalService.dismissAll() this.fastenApi.syncSource(source.id).subscribe( (respData) => { + delete this.status[source.id] delete this.status[source.source_type] console.log("source sync response:", respData) }, (err) => { + delete this.status[source.id] delete this.status[source.source_type] console.log(err) } diff --git a/frontend/src/app/models/events/event.ts b/frontend/src/app/models/events/event.ts new file mode 100644 index 00000000..ebdf30dd --- /dev/null +++ b/frontend/src/app/models/events/event.ts @@ -0,0 +1,3 @@ +export interface Event { + event_type: string; +} diff --git a/frontend/src/app/models/events/event_source_complete.ts b/frontend/src/app/models/events/event_source_complete.ts new file mode 100644 index 00000000..52d4393e --- /dev/null +++ b/frontend/src/app/models/events/event_source_complete.ts @@ -0,0 +1,4 @@ +export interface EventSourceComplete extends Event { + event_type: string; + source_id: string; +} diff --git a/frontend/src/app/models/events/event_source_sync.ts b/frontend/src/app/models/events/event_source_sync.ts new file mode 100644 index 00000000..81a63794 --- /dev/null +++ b/frontend/src/app/models/events/event_source_sync.ts @@ -0,0 +1,6 @@ +export interface EventSourceSync extends Event { + event_type: string; + source_id: string; + resource_type: string; + resource_id: string; +} diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index 1c365461..ec527a57 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -10,6 +10,7 @@ import * as jose from 'jose'; import {UserRegisteredClaims} from '../models/fasten/user-registered-claims'; import {uuidV4} from '../../lib/utils/uuid'; import {HTTP_CLIENT_TOKEN} from "../dependency-injection"; +import {BehaviorSubject, Observable, Subject} from 'rxjs'; @Injectable({ providedIn: 'root' @@ -17,7 +18,7 @@ import {HTTP_CLIENT_TOKEN} from "../dependency-injection"; export class AuthService { FASTEN_JWT_LOCALSTORAGE_KEY = 'token'; - + public IsAuthenticatedSubject = new BehaviorSubject(false) constructor(@Inject(HTTP_CLIENT_TOKEN) private _httpClient: HttpClient) { } @@ -129,17 +130,21 @@ export class AuthService { this.setAuthToken(resp.data) } - //TODO: now that we've moved to remote-first database, we can refactor and simplify this function significantly. public async IsAuthenticated(): Promise { let authToken = this.GetAuthToken() let hasAuthToken = !!authToken if(!hasAuthToken){ + this.publishAuthenticationState(false) return false } - //todo: check if the authToken has expired - return true + //check if the authToken has expired + let jwtClaims = jose.decodeJwt(authToken) + let valid = Date.now() < (jwtClaims.exp * 1000); + this.publishAuthenticationState(valid) + return valid + // //check if the authToken has expired. // let databaseEndpointBase = GetEndpointAbsolutePath(globalThis.location, environment.couchdb_endpoint_base) @@ -180,6 +185,7 @@ export class AuthService { } public async Logout(): Promise { + this.publishAuthenticationState(false) return localStorage.removeItem(this.FASTEN_JWT_LOCALSTORAGE_KEY) // // let remotePouchDb = new PouchDB(this.getRemoteUserDb(localStorage.getItem("current_user")), {skip_setup: true}); // if(this.pouchDb){ @@ -192,6 +198,13 @@ export class AuthService { ///////////////////////////////////////////////////////////////////////////////////////////////// private setAuthToken(token: string) { + this.publishAuthenticationState(true) localStorage.setItem(this.FASTEN_JWT_LOCALSTORAGE_KEY, token) } + + private publishAuthenticationState(authenticated){ + if(this.IsAuthenticatedSubject.value != authenticated){ + this.IsAuthenticatedSubject.next(authenticated) + } + } } diff --git a/frontend/src/app/services/event-bus.service.spec.ts b/frontend/src/app/services/event-bus.service.spec.ts new file mode 100644 index 00000000..574fc894 --- /dev/null +++ b/frontend/src/app/services/event-bus.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { EventBusService } from './event-bus.service'; + +describe('EventBusService', () => { + let service: EventBusService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EventBusService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/event-bus.service.ts b/frontend/src/app/services/event-bus.service.ts new file mode 100644 index 00000000..14c0f01e --- /dev/null +++ b/frontend/src/app/services/event-bus.service.ts @@ -0,0 +1,48 @@ +import {AfterViewInit, Injectable, OnInit} from '@angular/core'; +import {NavigationEnd, Router} from '@angular/router'; +import {BehaviorSubject, Observable, Subject, Subscription} from 'rxjs'; +import {AuthService} from './auth.service'; +import {FastenApiService} from './fasten-api.service'; +import {ToastService} from './toast.service'; +import {Event} from '../models/events/event'; +import {EventSourceComplete} from '../models/events/event_source_complete'; +import {EventSourceSync} from '../models/events/event_source_sync'; + +@Injectable({ + providedIn: 'root' +}) +export class EventBusService { + eventBusSubscription: Subscription | undefined; + eventBusSourceSyncMessages: Subject = new Subject(); + eventBusSourceCompleteMessages: Subject = new Subject(); + + constructor( + public router: Router, + public authService: AuthService, + public fastenApiService: FastenApiService, + public toastService: ToastService + ) { + + // Ideally we want consistently listen to events, but only when the user is authenticated. + //TODO: find a way to abort the event bus connection. + this.authService.IsAuthenticatedSubject.subscribe((isAuthenticated) => { + console.log("isAuthenticated changed:", isAuthenticated) + if(isAuthenticated){ + this.eventBusSubscription = this.fastenApiService.listenEventBus().subscribe((event: Event | EventSourceSync | EventSourceComplete)=>{ + console.log("eventbus event:", event) + //TODO: start toasts. + if(event.event_type == "source_sync"){ + this.eventBusSourceSyncMessages.next(event as EventSourceSync) + } else if(event.event_type == "source_complete"){ + this.eventBusSourceCompleteMessages.next(event as EventSourceComplete) + } + }) + } else { + //no longer authenticated, unsubscribe from eventbus + if(this.eventBusSubscription){ + this.eventBusSubscription.unsubscribe() + } + } + }); + } +} diff --git a/frontend/src/app/services/fasten-api.service.ts b/frontend/src/app/services/fasten-api.service.ts index 93184be2..5866b914 100644 --- a/frontend/src/app/services/fasten-api.service.ts +++ b/frontend/src/app/services/fasten-api.service.ts @@ -65,7 +65,7 @@ export class FastenApiService { 'Authorization': `Bearer ${this.authService.GetAuthToken()}` }, onmessage(ev) { - observer.next(ev.data); + observer.next(JSON.parse(ev.data)); }, onerror(event) { observer.error(event)