user.service.ts 13.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';
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
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);
Lukas Burgey's avatar
Lukas Burgey committed
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'),
Lukas Burgey's avatar
Lukas Burgey committed
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
Lukas Burgey's avatar
Lukas Burgey committed
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();
Lukas Burgey's avatar
Lukas Burgey committed
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 deleteUser() {
332
    return this.http.delete('/rest/user').pipe(
333
334
      catchError(this.handleError(true, "Error deleting user")),
    ).subscribe(
335
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
336
          this.user$.next(undefined);
Lukas Burgey's avatar
Lukas Burgey committed
337
          this.snackBar.open('Deleted user from server');
Lukas Burgey's avatar
Lukas Burgey committed
338
      },
339
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
340
341
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
342

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

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

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

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

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

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

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

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

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

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

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

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

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

Lukas Burgey's avatar
Lukas Burgey committed
473
474
475
476
477
478
    // 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,
        ),
479
      ),
Lukas Burgey's avatar
Lukas Burgey committed
480
481
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
482
}