user.service.ts 14.3 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 205 206
      if (update.msg) {
        this.snackBar.open(update.msg);
      }
      if (update.session && update.session.deactivated) {
        this.autoReLogin = false;
207
      }
Lukas Burgey's avatar
Lukas Burgey committed
208

Lukas Burgey's avatar
Lukas Burgey committed
209 210
      console.log(update.session);

Lukas Burgey's avatar
Lukas Burgey committed
211 212
      this.user = update.user;
      this.user$.next(this.user);
213 214

    } else {
Lukas Burgey's avatar
Lukas Burgey committed
215
      this.user$.next(undefined);
216 217 218
    }
  }

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

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

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

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

    console.log(error);
    this.snackBar.open('Error');
    this.fetch();
Lukas Burgey's avatar
Lukas Burgey committed
252
    return observableThrowError(error);
Lukas Burgey's avatar
Lukas Burgey committed
253 254
  }

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

Lukas Burgey's avatar
Lukas Burgey committed
268 269 270 271 272 273 274 275 276
  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
277
  // PUBLIC API
Lukas Burgey's avatar
Lukas Burgey committed
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  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;
          },
        ),
      );
  }


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

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

Lukas Burgey's avatar
Lukas Burgey committed
319
    console.log('Unable to login: No IdP');
320 321
  }

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

Lukas Burgey's avatar
Lukas Burgey committed
331
  public sentQuestionnaire(stateItemID: number, answers: JSONObject) {
332 333 334
    return this.http.patch<DeploymentState>(
      `/rest/dep-state?id=${ stateItemID }`,
      {
Lukas Burgey's avatar
Lukas Burgey committed
335
        'answers': answers,
336 337
      },
  ).pipe(
Lukas Burgey's avatar
Lukas Burgey committed
338
      catchError(this.handleError(true, "Error submitting answers")),
339
    ).subscribe(
340
      (state: DeploymentState) => this.updateDeploymentState(state),
341
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
342 343 344
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
345
  public deleteUser() {
346
    return this.http.delete('/rest/user').pipe(
347 348
      catchError(this.handleError(true, "Error deleting user")),
    ).subscribe(
349
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
350
          this.user$.next(undefined);
Lukas Burgey's avatar
Lukas Burgey committed
351
          this.snackBar.open('Deleted user from server');
Lukas Burgey's avatar
Lukas Burgey committed
352
      },
353
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
354 355
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
356

357 358
  public uploadSshKey(name: string, key: string | ArrayBuffer): void {
    this.http.post<SSHKey>('/rest/ssh-keys', {'name': name, 'key': key}).pipe(
359 360
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
361
      (newKey: SSHKey) => {
Lukas Burgey's avatar
Lukas Burgey committed
362 363
        this.sshKeys.set(newKey.id, newKey);
        this.sshKeys$.next(Array.from(this.sshKeys.values()));
364
      },
365
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
366 367 368
    );
  }

369
  public removeSshKey(key: SSHKey) {
Lukas Burgey's avatar
Lukas Burgey committed
370
    console.log('Deleting key:', key);
371
    return this.http.delete('/rest/ssh-key?id='+key.id.toString()).pipe(
372 373
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
374
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
375 376
        this.sshKeys.delete(key.id);
        this.sshKeys$.next(Array.from(this.sshKeys.values()));
377
      },
378
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
379 380
    );
  }
381

Lukas Burgey's avatar
Lukas Burgey committed
382
  public changeVODeployment(action: string, vo: VO): void {
383 384
    const body = {
      'type': action,
385
      'vo': vo.id,
386
    };
387
    this.http.post<Deployment>('/backend/api/deployments', body).pipe(
388
      catchError(this.handleError(true, "Error changing deployment")),
389 390
    ).subscribe(
      (dep: Deployment) => this.updateDeployment(dep),
391 392 393
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
394 395 396 397 398 399
  public changeServiceDeployment(action: string, service: Service): void {
    const body = {
      'type': action,
      'service': service.id,
    };
    this.http.post<Deployment>('/backend/api/deployments', body).pipe(
400
      catchError(this.handleError(true, "Error changing deployment")),
Lukas Burgey's avatar
Lukas Burgey committed
401 402 403 404 405
    ).subscribe(
      (dep: Deployment) => this.updateDeployment(dep),
    );
  }

406 407
  // DATA SERVICE API
  //
Lukas Burgey's avatar
Lukas Burgey committed
408
  public subscribeSpecific<T>(selector: (user: User) => T): Observable<T> {
409 410 411 412
    return this.userSrc().pipe(
      map(selector),
      tap(this.tapLogger('subscribeSpecific')),
    );
413 414
  }

Lukas Burgey's avatar
Lukas Burgey committed
415
  public sshKeysSrc(): Observable<SSHKey[]> {
416 417 418
    return this.sshKeys$.asObservable().pipe(
      tap(this.tapLogger('sshKeysSrc')),
    );
419 420
  }

Lukas Burgey's avatar
Lukas Burgey committed
421
  public userSrc(): Observable<User> {
422 423 424
    return this.user$.asObservable().pipe(
      tap(this.tapLogger('userSrc')),
    );
425 426
  }

427 428 429 430 431 432 433 434 435 436 437 438
  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
439
  public deploymentsSrc(): Observable<Deployment[]> {
440 441 442
    return this.deployments$.asObservable().pipe(
      tap(this.tapLogger('deploymentsSrc')),
    );
443 444
  }

Lukas Burgey's avatar
Lukas Burgey committed
445
  public depStatesSrc(): Observable<DeploymentState[]> {
446 447 448
    return this.deploymentStates$.asObservable().pipe(
      tap(this.tapLogger('depStatesSrc')),
    );
449 450
  }

Lukas Burgey's avatar
Lukas Burgey committed
451
  public subscribeStateFor(service: Service): Observable<DeploymentState> {
452 453
    return this.deploymentStates$.asObservable().pipe(
      map((states: DeploymentState[]) => states.find(
Lukas Burgey's avatar
Lukas Burgey committed
454
          (dsi: DeploymentState) => dsi.service.id == service.id,
455
      )),
456
      tap(this.tapLogger('subscribeStateFor')),
457 458 459
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
460
  public subscribeDeployment(selector: (dep: Deployment) => boolean): Observable<Deployment> {
Lukas Burgey's avatar
Lukas Burgey committed
461
    return this.deploymentsSrc().pipe(
Lukas Burgey's avatar
Lukas Burgey committed
462
      map((deployments: Deployment[]) => {
463
          return deployments.find(
Lukas Burgey's avatar
Lukas Burgey committed
464
            (dep: Deployment) => selector(dep),
465 466
          );
        }
467
      ),
468
      tap(this.tapLogger('subscribeDeployment')),
469 470 471
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
472
  public servicesSrc(): Observable<Service[]> {
473 474 475
    return this.subscribeSpecific(this.serviceSelector).pipe(
      tap(this.tapLogger('servicesSrc')),
    );
476
  }
Lukas Burgey's avatar
Lukas Burgey committed
477

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
  public extractVOs(combi: Combination): VO[] {
    if (combi.prefs.showEmptyVOs) {
      // 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,
          ),
        ),
      );
    }

    return combi.user.vos;

  }

Lukas Burgey's avatar
Lukas Burgey committed
494
  public vosSrc(): Observable<VO[]> {
Lukas Burgey's avatar
Lukas Burgey committed
495 496
    return this.prefs.connect().pipe(
      combineLatest(
Lukas Burgey's avatar
Lukas Burgey committed
497 498 499 500 501
        this.user$.asObservable(),
        (prefs: Prefs, user: User) => {
          const vos = this.voSelector(user);
          const services = this.serviceSelector(user);

Lukas Burgey's avatar
Lukas Burgey committed
502
          if (prefs.showEmptyVOs) {
Lukas Burgey's avatar
Lukas Burgey committed
503
            // filter out VOs that have no services
Lukas Burgey's avatar
Lukas Burgey committed
504
            return vos.filter(
Lukas Burgey's avatar
Lukas Burgey committed
505
              (vo: VO) => services.some(
Lukas Burgey's avatar
Lukas Burgey committed
506 507 508 509 510 511
                (s: Service) => s.vos.some(
                  svo => svo.id === vo.id,
                ),
              ),
            );
          }
Lukas Burgey's avatar
Lukas Burgey committed
512

Lukas Burgey's avatar
Lukas Burgey committed
513 514
          return vos;
        }
515
      ),
Lukas Burgey's avatar
Lukas Burgey committed
516 517
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
518
}