user.service.ts 13.5 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
// TODO terrible name
18
19
20
21
export interface Combination {
  user: User;
  prefs: Prefs;
}
Lukas Burgey's avatar
Lukas Burgey committed
22
23
24

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

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

  private observerDebugging = false;
31

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

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

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

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

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

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

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

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

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

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

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

111
112
113
114
115
116
117
          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
118
          this.loggedIn = true;
119
120
121
          this.initialized = true;
        }
      },
Lukas Burgey's avatar
Lukas Burgey committed
122
123
      this.handleError(true),
      () => console.log('user$ is complete'),
124
    );
Lukas Burgey's avatar
Lukas Burgey committed
125
126
  }

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

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

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

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

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

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

Lukas Burgey's avatar
Lukas Burgey committed
162
163
    subscription.subscribe(
      (message: Message) => {
164
165
166
        let update: Update = JSON.parse(message.body);
        console.log('update:', update);

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

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

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

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

193
194
195
196
197
198
199
  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
200
  private updateState(update: State): void {
201
202
    if (update) {
      // report an occured error
Lukas Burgey's avatar
Lukas Burgey committed
203
204
205
      if (update.msg) {
        this.snackBar.open(update.msg);
      }
Lukas Burgey's avatar
Lukas Burgey committed
206
207
208
209
      if (update.session) {
        if (update.session.deactivated) {
          this.autoReLogin = false;
        }
210
        if (update.session.auth_error && update.session.auth_error !== 'Not authenticated') {
Lukas Burgey's avatar
Lukas Burgey committed
211
212
          this.snackBar.open(update.session.auth_error);
        }
213
      }
Lukas Burgey's avatar
Lukas Burgey committed
214

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

Lukas Burgey's avatar
Lukas Burgey committed
388
389
390
391
392
393
  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
394
    ).subscribe(
Lukas Burgey's avatar
Lukas Burgey committed
395
396
      (state: DeploymentState) => this.updateDeploymentState(state),
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
397
398
399
    );
  }

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

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

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

421
422
423
424
425
426
427
428
429
430
431
432
  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
433
  public deploymentsSrc(): Observable<Deployment[]> {
434
435
436
    return this.deployments$.asObservable().pipe(
      tap(this.tapLogger('deploymentsSrc')),
    );
437
438
  }

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

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

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

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

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

Lukas Burgey's avatar
Lukas Burgey committed
477
478
479
480
481
482
    // 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,
        ),
483
      ),
Lukas Burgey's avatar
Lukas Burgey committed
484
485
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
486
}