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';
Lukas Burgey's avatar
Lukas Burgey committed
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[]>([]);
Lukas Burgey's avatar
Lukas Burgey committed
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(
Lukas Burgey's avatar
Lukas Burgey committed
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
    );
Lukas Burgey's avatar
Lukas Burgey committed
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

Lukas Burgey's avatar
Lukas Burgey committed
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
Lukas Burgey's avatar
Lukas Burgey committed
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
        }
Lukas Burgey's avatar
Lukas Burgey committed
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);
Lukas Burgey's avatar
Lukas Burgey committed
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'),
Lukas Burgey's avatar
Lukas Burgey committed
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
Lukas Burgey's avatar
Lukas Burgey committed
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();
Lukas Burgey's avatar
Lukas Burgey committed
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
    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);
          }
        }
      )
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 {
Lukas Burgey's avatar
Lukas Burgey committed
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);
Lukas Burgey's avatar
Lukas Burgey committed
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

Lukas Burgey's avatar
Lukas Burgey committed
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
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
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(
Lukas Burgey's avatar
Lukas Burgey committed
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
}