import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { throwError as observableThrowError, Observable, BehaviorSubject, of, EMPTY } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; import { CookieService } from 'ngx-cookie-service'; import { StompConfig, StompRService } from '@stomp/ng2-stompjs'; import { Message } from '@stomp/stompjs'; import { SnackBarService } from './snackbar.service'; import { IdpService } from './idp.service'; import { VO, User, Update, State, Deployment, DeploymentState, SSHKey, NewSSHKey, IdP, Service, Site } from './types/types.module'; @Injectable() export class UserService { private initialized = false; private loggedIn: boolean = false; private user: User; private user$: BehaviorSubject; private sshKeys: SSHKey[] = new Array(); private sshKeys$ = new BehaviorSubject([]); private deploymentStates: Map = new Map([]); private deploymentStates$ = new BehaviorSubject([]); private deployments: Map = new Map([]); private deployments$ = new BehaviorSubject([]); public voSelector = (user: User) => user ? user.vos : []; public serviceSelector = (user: User) => user ? user.services ? user.services : [] : []; constructor( private cookieService: CookieService, private http: HttpClient, private snackBar: SnackBarService, private idpService: IdpService, private stompService: StompRService, ) { this.user$ = new BehaviorSubject(null); this.connect(); } // PRIVATE API private connect(): void { this.fetch(); this.subscribeUser().subscribe( (newUser: User) => { console.log('user$:', newUser); if (newUser == undefined || newUser == null) { // LOGGED OUT // show the logout to the user if (this.loggedIn) { this.snackBar.open('Logged out'); // purge all values this.stompService.disconnect(); this.user = undefined; this.deployments = new Map([]); this.deployments$.next([]); this.sshKeys = []; this.sshKeys$.next([]); } this.loggedIn = false; } else { // LOGGED IN // show the login to the user if (!this.loggedIn) { this.snackBar.open('Logged in'); } if (newUser.id) { this.connectLiveUpdates(newUser.id); } if (newUser.ssh_keys) { this.sshKeys = newUser.ssh_keys; this.sshKeys$.next(this.sshKeys); } if (newUser.deployments) { newUser.deployments.forEach( (newDep: Deployment) => { this.deployments.set(newDep.id, newDep); } ); this.deployments$.next(Array.from(this.deployments.values())); } if (newUser.states) { newUser.states.forEach((state: DeploymentState) => { this.deploymentStates.set(state.id, state) }); this.deploymentStates$.next(Array.from(this.deploymentStates.values())); } this.loggedIn = true; this.initialized = true; } }, this.handleError(true), () => console.log('user$ is complete'), ); } private connectLiveUpdates(userID: number): void { // handle with care const login = userID const passcode = this.cookieService.get('sessionid'); const stompConfig: StompConfig = { // Which server? url: 'wss://'+window.location.host+'/ws', // Headers // Typical keys: login, passcode, host headers: { login: 'webpage-client:' + login, passcode: passcode, }, // How often to heartbeat? // Interval in milliseconds, set to 0 to disable heartbeat_in: 0, // Typical value 0 - disabled heartbeat_out: 20000, // Typical value 20000 - every 20 seconds // Wait in milliseconds before attempting auto reconnect // Set to 0 to disable // Typical value 15000 (15 seconds) reconnect_delay: 15000, // Will log diagnostics on console debug: false, }; this.stompService.config = stompConfig; this.stompService.initAndConnect(); const subscription = this.stompService.subscribe( '/exchange/users/' + userID.toString() ); subscription.subscribe( (message: Message) => { let update: Update = JSON.parse(message.body); console.log('update:', update); if (update.error && update.error != '') { this.snackBar.open(update.error); } if (update.deployment_state) { this.updateDeploymentState(update.deployment_state); } if (update.deployment) { this.updateDeployment(update.deployment); } }, this.logErrorAndFetch, ); } private updateDeployment(dep: Deployment): void { if (dep != undefined && dep != null) { dep.states.forEach((state: DeploymentState) => this.updateDeploymentState(state), ); this.deployments.set(dep.id, dep); this.deployments$.next(Array.from(this.deployments.values())); } } private updateDeploymentState(ds: DeploymentState): void { if (ds != undefined && ds != null) { this.deploymentStates.set(ds.id, ds); this.deploymentStates$.next(Array.from(this.deploymentStates.values())); } } private updateState(update: State): void { if (update) { // report an occured error if (update.error) { this.snackBar.open(update.error); } this.user = update.user; this.user$.next(this.user); } else { this.user$.next(undefined); } } private fetch(): void { this.http.get('/backend/api/state').subscribe( (state: State) => this.updateState(state), this.handleError(false, 'Error fetching state. Try again later'), ); } private handleError(fetch: boolean, msg?: string) { return (error: any) => { if (error.status == 403) { this.login(); return } if (msg) { this.snackBar.open(msg); } console.log('fetch:', error); if (fetch) { this.fetch(); } } } private catchError(fetch: boolean, msg?: string) { return (error: any) => { this.handleError(fetch, msg)(error); return EMPTY; } } private logErrorAndFetch(error: any) { if (error.status === 403) { this.login(); } console.log(error); this.snackBar.open('Error'); this.fetch(); return observableThrowError(error); } // PUBLIC API public serviceDescription(service: Service): string { if (service.description != "") { return service.description; } return "No description"; } public login(idp?: IdP): void { if (idp) { this.idpService.setIdPPreference(idp); } window.location.href = '/backend/auth/v1/request'; } public logout(): void { this.http.post('/backend/auth/v1/logout', undefined).subscribe( (state: State) => this.updateState(state), (err: HttpErrorResponse) => { this.updateState(undefined); } ); } public sentQuestionnaire(stateItemID: number, answers: Object) { return this.http.post('/backend/api/questionnaire?id='+String(stateItemID), answers).subscribe( (dep: Deployment) => this.updateDeployment(dep), this.logErrorAndFetch, ); } public deleteUser() { return this.http.delete('/backend/api/delete_user').subscribe( (data: {deleted: boolean}) => { if (data && data.deleted) { this.user$.next(undefined); this.snackBar.open('Deleted user from server'); } }, this.logErrorAndFetch, ); } public uploadSshKey(formData: FormData): void { this.http.post('/backend/api/sshkey', formData).subscribe( (newKey: SSHKey) => { this.sshKeys.push(newKey); this.sshKeys$.next(this.sshKeys); }, this.logErrorAndFetch, ); } public removeSshKey(key: SSHKey) { console.log('Deleting key:', key); return this.http.post('/backend/api/sshkey', { 'type': 'remove', 'id': key.id, }).subscribe( (data: {deleted: boolean}) => { if (data && data.deleted) { this.sshKeys = this.sshKeys.filter( k => key.id != k.id ); this.sshKeys$.next(this.sshKeys); } }, this.logErrorAndFetch, ); } public changeDeployment(action: string, vo: VO): void { const body = { 'type': action, 'vo': vo.id, }; this.http.post('/backend/api/deployments', body).pipe( catchError(this.catchError(true, "Error changing deployment")), ).subscribe( (dep: Deployment) => this.updateDeployment(dep), ); } // DATA SERVICE API // public subscribeSpecific(selector: (user: User) => T): Observable { return this.subscribeUser().pipe(map(selector)); } public subscribeSSHKeys(): Observable { return this.sshKeys$.asObservable(); } public subscribeUser(): Observable { return this.user$.asObservable(); } public subscribeDeployments(): Observable { return this.deployments$.asObservable(); } public subscribeDeploymentStates(): Observable { return this.deploymentStates$.asObservable(); } public subscribeStateFor(site: Site, service: Service): Observable { return this.deploymentStates$.asObservable().pipe( map((states: DeploymentState[]) => states.find( (dsi: DeploymentState) => dsi.site.id == site.id && dsi.service.id == service.id, )), ); } public subscribeVOs(): Observable { let voSelector = (user: User) => user ? user.vos : []; return this.subscribeSpecific(voSelector); } public subscribeServiceDeployment(service: Service): Observable { return this.subscribeDeployments().pipe( map((deployments: Deployment[]) => { return deployments.find( (dep: Deployment) => dep.service ? dep.service.id == service.id : false ); } ), ); } public subscribeVODeployment(vo: VO): Observable { return this.subscribeDeployments().pipe( map((deployments: Deployment[]) => { return deployments.find( (dep: Deployment) => dep.vo ? dep.vo.id == vo.id : false ); } ), ); } }