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 {
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
        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
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
  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
307
    const redirect = (arg: IdP) => {
      window.location.href = '/?idphint=' + encodeURIComponent(arg.issuer_uri);
Lukas Burgey's avatar
Lukas Burgey committed
308
309
    };

Lukas Burgey's avatar
Lukas Burgey committed
310
    if (idp !== undefined) {
Lukas Burgey's avatar
Lukas Burgey committed
311
312
313
314
315
      this.prefs.setPreferredIdP(idp);
      redirect(idp);
    } else {
      this.prefs.connect().subscribe(
        prefs => {
Lukas Burgey's avatar
Lukas Burgey committed
316
          if (prefs.preferredIdP !== undefined) {
Lukas Burgey's avatar
Lukas Burgey committed
317
318
319
            redirect(prefs.preferredIdP);
          }
        }
Lukas Burgey's avatar
Lukas Burgey committed
320
      );
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(
Lukas Burgey's avatar
Lukas Burgey committed
337
      catchError(this.handleError(true, 'Error deleting user')),
338
    ).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(
Lukas Burgey's avatar
Lukas Burgey committed
352
      catchError(this.handleError(true, 'Error changing deployment')),
353
    ).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(
Lukas Burgey's avatar
Lukas Burgey committed
367
      catchError(this.handleError(true, 'Error changing deployment')),
368
    ).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(
Lukas Burgey's avatar
Lukas Burgey committed
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
  public sentQuestionnaire(stateItemID: number, answers: JSONObject) {
    return this.http.patch<DeploymentState>(
      `/rest/state/${ stateItemID }`,
      {'answers': answers},
    ).pipe(
Lukas Burgey's avatar
Lukas Burgey committed
393
      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
}