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
  private user$: BehaviorSubject<User> = new BehaviorSubject(undefined);
Lukas Burgey's avatar
Lukas Burgey committed
38
  private userEmissions = 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
        if (newUser === undefined || newUser == null) {
Lukas Burgey's avatar
Lukas Burgey committed
70
71
72
73
74
75
76
77
78
79
          // 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
          if (newUser.states) {
            newUser.states.forEach((state: DeploymentState) => {
Lukas Burgey's avatar
Lukas Burgey committed
113
              this.deploymentStates.set(state.id, state);
114
115
116
117
            });
            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
Lukas Burgey's avatar
Lukas Burgey committed
129
    const login = userID;
130
    const passcode = this.cookieService.get('sessionid');
Lukas Burgey's avatar
Lukas Burgey committed
131
132
    const stompConfig: StompConfig = {
      // Which server?
Lukas Burgey's avatar
Lukas Burgey committed
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) => {
Lukas Burgey's avatar
Lukas Burgey committed
164
        const update: Update = JSON.parse(message.body);
165
166
        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
        if (update.error && update.error !== '') {
Lukas Burgey's avatar
Lukas Burgey committed
169
          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 {
Lukas Burgey's avatar
Lukas Burgey committed
185
    if (dep !== undefined && dep !== null) {
186
187
      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
  private updateDeploymentState(ds: DeploymentState): void {
Lukas Burgey's avatar
Lukas Burgey committed
194
    if (ds !== undefined && ds !== null) {
195
196
197
198
199
      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 {
224
    this.http.get<State>('/webpage/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
        this.login();
Lukas Burgey's avatar
Lukas Burgey committed
234
        return;
Lukas Burgey's avatar
Lukas Burgey committed
235
      }
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();
      }
Lukas Burgey's avatar
Lukas Burgey committed
244
245
      return EMPTY;
    };
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
      }
Lukas Burgey's avatar
Lukas Burgey committed
269
    };
270
271
  }

Lukas Burgey's avatar
Lukas Burgey committed
272
  private fetchIdPInfo(): void {
273
      this.http.get<IdPInfo>('/auth/info').subscribe(
Lukas Burgey's avatar
Lukas Burgey committed
274
275
276
277
278
279
280
        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
  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);

Lukas Burgey's avatar
Lukas Burgey committed
289
            if (prefs !== undefined && prefs.preferredIdP !== undefined) {
Lukas Burgey's avatar
Lukas Burgey committed
290
291
292
293
294
295
              authInfo.selectedIdP = prefs.preferredIdP;
            } else if (defaultIdP) {
              authInfo.selectedIdP = defaultIdP;
            } else if (authInfo.idps.length > 0) {
              authInfo.selectedIdP = authInfo.idps[0];
            } else {
Lukas Burgey's avatar
Lukas Burgey committed
296
              // console.log("No IdPs available. Unable to login");
Lukas Burgey's avatar
Lukas Burgey committed
297
298
299
300
301
302
303
304
            }
            return authInfo;
          },
        ),
      );
  }


305
  public login(idp?: IdP): void {
Lukas Burgey's avatar
Lukas Burgey committed
306
    const redirect = (arg: IdP) => {
Lukas Burgey's avatar
Lukas Burgey committed
307
      window.location.href = '/?idp=' + encodeURIComponent(arg.issuer_uri);
Lukas Burgey's avatar
Lukas Burgey committed
308
309
    };

310
    // was there an argument?
Lukas Burgey's avatar
Lukas Burgey committed
311
    if (idp !== undefined) {
Lukas Burgey's avatar
Lukas Burgey committed
312
313
      this.prefs.setPreferredIdP(idp);
      redirect(idp);
314
      return;
315
    }
316

317
318
319
320
321
322
323
324
325
326
    // is the a preference?
    this.prefs.connect().subscribe(
      prefs => {
        if (prefs.preferredIdP !== undefined) {
          redirect(prefs.preferredIdP);
          return;
        }
      }
    );

Lukas Burgey's avatar
Lukas Burgey committed
327
    console.log('Unable to login: No IdP');
328
329
  }

Lukas Burgey's avatar
Lukas Burgey committed
330
  public logout(): void {
331
    this.http.post<State>('/auth/logout', undefined).subscribe(
332
      (state: State) => this.updateState(state),
Lukas Burgey's avatar
Lukas Burgey committed
333
334
335
      (err: HttpErrorResponse) => {
        this.updateState(undefined);
      }
336
337
338
    );
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

476
477
  public extractVOs(combi: Combination): VO[] {
    if (combi.prefs.showEmptyVOs) {
Lukas Burgey's avatar
Lukas Burgey committed
478
      return combi.user.vos;
479
480
    }

Lukas Burgey's avatar
Lukas Burgey committed
481
482
483
484
485
486
    // 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,
        ),
487
      ),
Lukas Burgey's avatar
Lukas Burgey committed
488
489
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
490
}