user.service.ts 13.4 KB
Newer Older
Lukas Burgey's avatar
Lukas Burgey committed
1 2
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
Lukas Burgey's avatar
Lukas Burgey committed
3
import { Observable, BehaviorSubject, AsyncSubject, throwError as observableThrowError,  of, EMPTY } from 'rxjs';
4
import { map, catchError, combineLatest, tap } from 'rxjs/operators';
5

Lukas Burgey's avatar
Lukas Burgey committed
6 7 8 9 10
import { CookieService } from 'ngx-cookie-service';
import { StompConfig, StompRService } from '@stomp/ng2-stompjs';
import { Message } from '@stomp/stompjs';

import { SnackBarService } from './snackbar.service';
Lukas Burgey's avatar
Lukas Burgey committed
11
import { PreferencesService, Prefs } from './preferences/preferences.service';
Lukas Burgey's avatar
Lukas Burgey committed
12

Lukas Burgey's avatar
Lukas Burgey committed
13
import {
Lukas Burgey's avatar
Lukas Burgey committed
14
  VO, User, Update, State, Deployment, DeploymentState, SSHKey, NewSSHKey, IdP, Service, Site, JSONObject, IdPInfo
Lukas Burgey's avatar
Lukas Burgey committed
15
} from './types/types.module';
Lukas Burgey's avatar
Lukas Burgey committed
16

17 18 19 20
export interface Combination {
  user: User;
  prefs: Prefs;
}
Lukas Burgey's avatar
Lukas Burgey committed
21 22 23

@Injectable()
export class UserService {
24
  private initialized = false;
Lukas Burgey's avatar
Lukas Burgey committed
25 26
  private loggedIn = false;

Lukas Burgey's avatar
Lukas Burgey committed
27 28 29
  private idpInfo: AsyncSubject<IdPInfo> = new AsyncSubject();

  private observerDebugging = false;
30

Lukas Burgey's avatar
Lukas Burgey committed
31 32 33
  // relogin on failed XHR calls
  // is turned off when the user is deactivated
  private autoReLogin = true;
Lukas Burgey's avatar
Lukas Burgey committed
34

35
  private user: User;
36 37
  private user$: BehaviorSubject<User> = new BehaviorSubject(undefined);
  private userEmissions: number = 0;
Lukas Burgey's avatar
Lukas Burgey committed
38

Lukas Burgey's avatar
Lukas Burgey committed
39
  private sshKeys: Map<number, SSHKey> = new Map([]);
Lukas Burgey's avatar
Lukas Burgey committed
40
  private sshKeys$ = new BehaviorSubject<SSHKey[]>([]);
Lukas Burgey's avatar
Lukas Burgey committed
41

42 43 44
  private deploymentStates: Map<number, DeploymentState> = new Map([]);
  private deploymentStates$ = new BehaviorSubject<DeploymentState[]>([]);

45
  private deployments: Map<number, Deployment> = new Map([]);
Lukas Burgey's avatar
Lukas Burgey committed
46
  private deployments$ = new BehaviorSubject<Deployment[]>([]);
47

48
  public voSelector = (user: User) => user ? user.vos : [];
Lukas Burgey's avatar
Lukas Burgey committed
49
  public serviceSelector = (user: User) => user ? user.services : [];
Lukas Burgey's avatar
Lukas Burgey committed
50 51

  constructor(
52 53 54
    private cookieService: CookieService,
    private http: HttpClient,
    private snackBar: SnackBarService,
Lukas Burgey's avatar
Lukas Burgey committed
55
    private stompService: StompRService,
Lukas Burgey's avatar
Lukas Burgey committed
56
    private prefs: PreferencesService,
Lukas Burgey's avatar
Lukas Burgey committed
57
  ) {
58
    this.connect();
59 60
  }

61 62
  // PRIVATE API
  private connect(): void {
Lukas Burgey's avatar
Lukas Burgey committed
63
    this.fetch();
Lukas Burgey's avatar
Lukas Burgey committed
64
    this.fetchIdPInfo();
Lukas Burgey's avatar
Lukas Burgey committed
65

Lukas Burgey's avatar
Lukas Burgey committed
66
    this.userSrc().subscribe(
67
      (newUser: User) => {
Lukas Burgey's avatar
Lukas Burgey committed
68 69 70 71 72 73 74 75 76 77 78
        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([]);
Lukas Burgey's avatar
Lukas Burgey committed
79
            this.sshKeys = new Map([]);
Lukas Burgey's avatar
Lukas Burgey committed
80 81 82 83 84 85 86 87 88 89 90
            this.sshKeys$.next([]);
          }

          this.loggedIn =  false;
        } else {
          // LOGGED IN
          // show the login to the user
          if (!this.loggedIn) {
            this.snackBar.open('Logged in');
          }

91 92 93 94
          if (newUser.id) {
            this.connectLiveUpdates(newUser.id);
          }
          if (newUser.ssh_keys) {
Lukas Burgey's avatar
Lukas Burgey committed
95 96 97 98 99
            this.sshKeys = new Map([]);
            newUser.ssh_keys.forEach(
              (key: SSHKey) => this.sshKeys.set(key.id, key),
            );
            this.sshKeys$.next(Array.from(this.sshKeys.values()));
100 101 102 103 104 105 106 107 108 109
          }
          if (newUser.deployments) {
            newUser.deployments.forEach(
              (newDep: Deployment) => {
                this.deployments.set(newDep.id,  newDep);
              }
            );
            this.deployments$.next(Array.from(this.deployments.values()));
          }

110 111 112 113 114 115 116
          if (newUser.states) {
            newUser.states.forEach((state: DeploymentState) => {
              this.deploymentStates.set(state.id, state)
            });
            this.deploymentStates$.next(Array.from(this.deploymentStates.values()));
          }

Lukas Burgey's avatar
Lukas Burgey committed
117
          this.loggedIn = true;
118 119 120
          this.initialized = true;
        }
      },
Lukas Burgey's avatar
Lukas Burgey committed
121 122
      this.handleError(true),
      () => console.log('user$ is complete'),
123
    );
124 125
  }

126
  private connectLiveUpdates(userID: number): void {
Lukas Burgey's avatar
Lukas Burgey committed
127
    // handle with care
128 129
    const login = userID
    const passcode = this.cookieService.get('sessionid');
Lukas Burgey's avatar
Lukas Burgey committed
130 131
    const stompConfig: StompConfig = {
      // Which server?
132
      url: 'wss://'+window.location.host+'/ws',
133

Lukas Burgey's avatar
Lukas Burgey committed
134 135 136 137 138 139
      // Headers
      // Typical keys: login, passcode, host
      headers: {
        login: 'webpage-client:' + login,
        passcode: passcode,
      },
Lukas Burgey's avatar
Lukas Burgey committed
140

Lukas Burgey's avatar
Lukas Burgey committed
141 142 143 144 145 146
      // 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
147 148
      // Typical value 15000 (15 seconds)
      reconnect_delay: 15000,
149

Lukas Burgey's avatar
Lukas Burgey committed
150 151 152
      // Will log diagnostics on console
      debug: false,
    };
153

Lukas Burgey's avatar
Lukas Burgey committed
154 155
    this.stompService.config = stompConfig;
    this.stompService.initAndConnect();
Lukas Burgey's avatar
Lukas Burgey committed
156

157
    const subscription = this.stompService.subscribe(
158
      '/exchange/users/' + userID.toString()
Lukas Burgey's avatar
Lukas Burgey committed
159
    );
160

161 162
    subscription.subscribe(
      (message: Message) => {
163 164 165
        let update: Update = JSON.parse(message.body);
        console.log('update:', update);

Lukas Burgey's avatar
Lukas Burgey committed
166
        // TODO rename error to msg
167 168
        if (update.error && update.error != '') {
          this.snackBar.open(update.error);
Lukas Burgey's avatar
Lukas Burgey committed
169
        }
170

171 172 173 174
        if (update.deployment_state) {
          this.updateDeploymentState(update.deployment_state);
        }

175 176
        if (update.deployment) {
          this.updateDeployment(update.deployment);
Lukas Burgey's avatar
Lukas Burgey committed
177
        }
178
      },
179
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
180
    );
Lukas Burgey's avatar
Lukas Burgey committed
181
  }
182

Lukas Burgey's avatar
Lukas Burgey committed
183
  private updateDeployment(dep: Deployment): void {
184 185 186
    if (dep != undefined && dep != null) {
      dep.states.forEach((state: DeploymentState) => this.updateDeploymentState(state),
      );
Lukas Burgey's avatar
Lukas Burgey committed
187 188 189 190
      this.deployments.set(dep.id, dep);
      this.deployments$.next(Array.from(this.deployments.values()));
    }
  }
Lukas Burgey's avatar
Lukas Burgey committed
191

192 193 194 195 196 197 198
  private updateDeploymentState(ds: DeploymentState): void {
    if (ds != undefined && ds != null) {
      this.deploymentStates.set(ds.id, ds);
      this.deploymentStates$.next(Array.from(this.deploymentStates.values()));
    }
  }

Lukas Burgey's avatar
Lukas Burgey committed
199
  private updateState(update: State): void {
200 201
    if (update) {
      // report an occured error
Lukas Burgey's avatar
Lukas Burgey committed
202 203 204
      if (update.msg) {
        this.snackBar.open(update.msg);
      }
Lukas Burgey's avatar
Lukas Burgey committed
205 206 207 208 209 210 211
      if (update.session) {
        if (update.session.deactivated) {
          this.autoReLogin = false;
        }
        if (update.session.auth_error) {
          this.snackBar.open(update.session.auth_error);
        }
212
      }
Lukas Burgey's avatar
Lukas Burgey committed
213

Lukas Burgey's avatar
Lukas Burgey committed
214 215
      this.user = update.user;
      this.user$.next(this.user);
216 217

    } else {
Lukas Burgey's avatar
Lukas Burgey committed
218
      this.user$.next(undefined);
219 220 221
    }
  }

222
  private fetch(): void {
Lukas Burgey's avatar
Lukas Burgey committed
223
    this.http.get<State>('/backend/api/state').subscribe(
224
      (state: State) => this.updateState(state),
Lukas Burgey's avatar
Lukas Burgey committed
225
      this.handleError(false, 'Error fetching state. Try again later'),
226 227 228
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
229 230
  private handleError(fetch: boolean, msg?: string) {
    return (error: any) => {
Lukas Burgey's avatar
Lukas Burgey committed
231
      if (error.status === 403 && this.autoReLogin) {
Lukas Burgey's avatar
Lukas Burgey committed
232 233 234
        this.login();
        return
      }
235

Lukas Burgey's avatar
Lukas Burgey committed
236 237 238 239 240 241 242
      if (msg) {
        this.snackBar.open(msg);
      }
      console.log('fetch:', error);
      if (fetch) {
        this.fetch();
      }
243
      return EMPTY
244
    }
Lukas Burgey's avatar
Lukas Burgey committed
245 246
  }

Lukas Burgey's avatar
Lukas Burgey committed
247
  private logErrorAndFetch(error: any)  {
Lukas Burgey's avatar
Lukas Burgey committed
248
    if (error.status === 403 && this.autoReLogin) {
249
      this.login();
250
    }
251 252 253 254

    console.log(error);
    this.snackBar.open('Error');
    this.fetch();
Lukas Burgey's avatar
Lukas Burgey committed
255
    return observableThrowError(error);
Lukas Burgey's avatar
Lukas Burgey committed
256 257
  }

258 259 260
  private tapLogger(name: string): (e: any) => void {
    return e => {
      if (this.observerDebugging) {
Lukas Burgey's avatar
Lukas Burgey committed
261 262 263 264 265 266
        if (name === 'userSrc') {
          this.userEmissions++;
          console.log(name, this.userEmissions, e);
        } else {
          console.log(name, e);
        }
267 268 269 270
      }
    }
  }

Lukas Burgey's avatar
Lukas Burgey committed
271 272 273 274 275 276 277 278 279
  private fetchIdPInfo(): void {
      this.http.get<IdPInfo>('/backend/auth/v1/info').subscribe(
        idpInfo => {
          this.idpInfo.next(idpInfo);
          this.idpInfo.complete();
        }
      );
  }

Lukas Burgey's avatar
Lukas Burgey committed
280
  // PUBLIC API
Lukas Burgey's avatar
Lukas Burgey committed
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
  public connectIdPInfo(): Observable<IdPInfo> {
      return this.idpInfo.asObservable().pipe(
        combineLatest(
          this.prefs.connect(),
          (authInfo, prefs) => {
            const defaultIdP: IdP = authInfo.idps.find((idp: IdP) => idp.id === authInfo.default);

            if (prefs != undefined && prefs.preferredIdP != undefined) {
              authInfo.selectedIdP = prefs.preferredIdP;
            } else if (defaultIdP) {
              authInfo.selectedIdP = defaultIdP;
            } else if (authInfo.idps.length > 0) {
              authInfo.selectedIdP = authInfo.idps[0];
            } else {
              console.log("No IdPs available. Unable to login");
            }
            return authInfo;
          },
        ),
      );
  }


304
  public login(idp?: IdP): void {
Lukas Burgey's avatar
Lukas Burgey committed
305
    const redirect = (idp: IdP) => {
Lukas Burgey's avatar
Lukas Burgey committed
306
      window.location.href = '/?idphint=' + encodeURIComponent(idp.issuer_uri);
Lukas Burgey's avatar
Lukas Burgey committed
307 308 309 310 311 312 313 314 315 316 317 318 319
    };

    if (idp != undefined) {
      this.prefs.setPreferredIdP(idp);
      redirect(idp);
    } else {
      this.prefs.connect().subscribe(
        prefs => {
          if (prefs.preferredIdP != undefined) {
            redirect(prefs.preferredIdP);
          }
        }
      )
320
    }
321

Lukas Burgey's avatar
Lukas Burgey committed
322
    console.log('Unable to login: No IdP');
323 324
  }

Lukas Burgey's avatar
Lukas Burgey committed
325
  public logout(): void {
Lukas Burgey's avatar
Lukas Burgey committed
326
    this.http.post<State>('/backend/auth/v1/logout', undefined).subscribe(
327
      (state: State) => this.updateState(state),
Lukas Burgey's avatar
Lukas Burgey committed
328 329 330
      (err: HttpErrorResponse) => {
        this.updateState(undefined);
      }
331 332 333
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
334
  public deleteUser() {
335
    return this.http.delete('/rest/user').pipe(
336 337
      catchError(this.handleError(true, "Error deleting user")),
    ).subscribe(
338
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
339
          this.user$.next(undefined);
Lukas Burgey's avatar
Lukas Burgey committed
340
          this.snackBar.open('Deleted user from server');
Lukas Burgey's avatar
Lukas Burgey committed
341
      },
342
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
343 344
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
345

346
  public uploadSshKey(name: string, key: string | ArrayBuffer): void {
347 348 349 350
    this.http.post<SSHKey>(
      '/rest/ssh-keys',
      {'name': name, 'key': key},
    ).pipe(
351 352
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
353
      (newKey: SSHKey) => {
Lukas Burgey's avatar
Lukas Burgey committed
354 355
        this.sshKeys.set(newKey.id, newKey);
        this.sshKeys$.next(Array.from(this.sshKeys.values()));
356
      },
357
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
358 359 360
    );
  }

361
  public removeSshKey(key: SSHKey) {
Lukas Burgey's avatar
Lukas Burgey committed
362
    console.log('Deleting key:', key);
363 364 365
    return this.http.delete(
      `/rest/ssh-key/${ key.id.toString() }`,
    ).pipe(
366 367
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
368
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
369 370
        this.sshKeys.delete(key.id);
        this.sshKeys$.next(Array.from(this.sshKeys.values()));
371
      },
372
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
373 374
    );
  }
375

376 377 378 379 380
  public patchDeployment(depType: string, stateTarget: string, id: number): void {
    this.http.patch<Deployment>(
      `/rest/deployment/${ depType }/${ id.toString() }`,
      {'state_target': stateTarget},
    ).pipe(
381
      catchError(this.handleError(true, "Error changing deployment")),
382 383
    ).subscribe(
      (dep: Deployment) => this.updateDeployment(dep),
384 385 386
    );
  }

387 388 389 390 391 392
  public sentQuestionnaire(stateItemID: number, answers: JSONObject) {
    return this.http.patch<DeploymentState>(
      `/rest/state/${ stateItemID }`,
      {'answers': answers},
    ).pipe(
      catchError(this.handleError(true, "Error submitting answers")),
Lukas Burgey's avatar
Lukas Burgey committed
393
    ).subscribe(
394 395
      (state: DeploymentState) => this.updateDeploymentState(state),
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
396 397 398
    );
  }

399 400
  // DATA SERVICE API
  //
Lukas Burgey's avatar
Lukas Burgey committed
401
  public subscribeSpecific<T>(selector: (user: User) => T): Observable<T> {
402 403 404 405
    return this.userSrc().pipe(
      map(selector),
      tap(this.tapLogger('subscribeSpecific')),
    );
406 407
  }

Lukas Burgey's avatar
Lukas Burgey committed
408
  public sshKeysSrc(): Observable<SSHKey[]> {
409 410 411
    return this.sshKeys$.asObservable().pipe(
      tap(this.tapLogger('sshKeysSrc')),
    );
412 413
  }

Lukas Burgey's avatar
Lukas Burgey committed
414
  public userSrc(): Observable<User> {
415 416 417
    return this.user$.asObservable().pipe(
      tap(this.tapLogger('userSrc')),
    );
418 419
  }

420 421 422 423 424 425 426 427 428 429 430 431
  public combiSrc(): Observable<Combination> {
    return this.user$.asObservable().pipe(
      combineLatest(
        this.prefs.connect(),
        (u, p) => {
          return {user: u, prefs: p};
        },
      ),
      tap(this.tapLogger('userSrc')),
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
432
  public deploymentsSrc(): Observable<Deployment[]> {
433 434 435
    return this.deployments$.asObservable().pipe(
      tap(this.tapLogger('deploymentsSrc')),
    );
436 437
  }

Lukas Burgey's avatar
Lukas Burgey committed
438
  public depStatesSrc(): Observable<DeploymentState[]> {
439 440 441
    return this.deploymentStates$.asObservable().pipe(
      tap(this.tapLogger('depStatesSrc')),
    );
442 443
  }

Lukas Burgey's avatar
Lukas Burgey committed
444
  public subscribeStateFor(service: Service): Observable<DeploymentState> {
445 446
    return this.deploymentStates$.asObservable().pipe(
      map((states: DeploymentState[]) => states.find(
Lukas Burgey's avatar
Lukas Burgey committed
447
          (dsi: DeploymentState) => dsi.service.id == service.id,
448
      )),
449
      tap(this.tapLogger('subscribeStateFor')),
450 451 452
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
453
  public subscribeDeployment(selector: (dep: Deployment) => boolean): Observable<Deployment> {
Lukas Burgey's avatar
Lukas Burgey committed
454
    return this.deploymentsSrc().pipe(
Lukas Burgey's avatar
Lukas Burgey committed
455
      map((deployments: Deployment[]) => {
456
          return deployments.find(
Lukas Burgey's avatar
Lukas Burgey committed
457
            (dep: Deployment) => selector(dep),
458 459
          );
        }
460
      ),
461
      tap(this.tapLogger('subscribeDeployment')),
462 463 464
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
465
  public servicesSrc(): Observable<Service[]> {
466 467 468
    return this.subscribeSpecific(this.serviceSelector).pipe(
      tap(this.tapLogger('servicesSrc')),
    );
469
  }
Lukas Burgey's avatar
Lukas Burgey committed
470

471 472
  public extractVOs(combi: Combination): VO[] {
    if (combi.prefs.showEmptyVOs) {
Lukas Burgey's avatar
Lukas Burgey committed
473
      return combi.user.vos;
474 475
    }

Lukas Burgey's avatar
Lukas Burgey committed
476 477 478 479 480 481
    // filter out VOs that have no services
    return combi.user.vos.filter(
      (vo: VO) => combi.user.services.some(
        (s: Service) => s.vos.some(
          svo => svo.id === vo.id,
        ),
482
      ),
Lukas Burgey's avatar
Lukas Burgey committed
483 484
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
485
}