user.service.ts 11.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 { throwError as observableThrowError,  Observable, BehaviorSubject, of, EMPTY } from 'rxjs';
Lukas Burgey's avatar
Lukas Burgey committed
4
import { map, catchError, combineLatest } from 'rxjs/operators';
Lukas Burgey's avatar
Lukas Burgey committed
5

Lukas Burgey's avatar
Lukas Burgey committed
6
7
8
9
10
11
import { CookieService } from 'ngx-cookie-service';
import { StompConfig, StompRService } from '@stomp/ng2-stompjs';
import { Message } from '@stomp/stompjs';

import { SnackBarService } from './snackbar.service';
import { IdpService } from './idp.service';
Lukas Burgey's avatar
Lukas Burgey committed
12
import { PreferencesService, Prefs } from './preferences/preferences.service';
Lukas Burgey's avatar
Lukas Burgey committed
13

Lukas Burgey's avatar
Lukas Burgey committed
14
import {
Lukas Burgey's avatar
Lukas Burgey committed
15
  VO, User, Update, State, Deployment, DeploymentState, SSHKey, NewSSHKey, IdP, Service, Site, JSONObject
Lukas Burgey's avatar
Lukas Burgey committed
16
} from './types/types.module';
Lukas Burgey's avatar
Lukas Burgey committed
17

Lukas Burgey's avatar
Lukas Burgey committed
18
19
20

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

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

28
  private user: User;
Lukas Burgey's avatar
Lukas Burgey committed
29
  private user$: BehaviorSubject<User>;
Lukas Burgey's avatar
Lukas Burgey committed
30

31
  private sshKeys: SSHKey[] = new Array<SSHKey>();
Lukas Burgey's avatar
Lukas Burgey committed
32
  private sshKeys$ = new BehaviorSubject<SSHKey[]>([]);
Lukas Burgey's avatar
Lukas Burgey committed
33

34
35
36
  private deploymentStates: Map<number, DeploymentState> = new Map([]);
  private deploymentStates$ = new BehaviorSubject<DeploymentState[]>([]);

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

40
  public voSelector = (user: User) => user ? user.vos : [];
Lukas Burgey's avatar
Lukas Burgey committed
41
  public serviceSelector = (user: User) => user ? user.services : [];
Lukas Burgey's avatar
Lukas Burgey committed
42
43

  constructor(
Lukas Burgey's avatar
Lukas Burgey committed
44
45
46
47
    private cookieService: CookieService,
    private http: HttpClient,
    private snackBar: SnackBarService,
    private idpService: IdpService,
Lukas Burgey's avatar
Lukas Burgey committed
48
    private stompService: StompRService,
Lukas Burgey's avatar
Lukas Burgey committed
49
    private prefs: PreferencesService,
Lukas Burgey's avatar
Lukas Burgey committed
50
  ) {
Lukas Burgey's avatar
Lukas Burgey committed
51
    this.user$ = new BehaviorSubject(null);
52
    this.connect();
53
54
  }

55
56
  // PRIVATE API
  private connect(): void {
Lukas Burgey's avatar
Lukas Burgey committed
57
58
    this.fetch();

59
60
    this.subscribeUser().subscribe(
      (newUser: User) => {
61
        console.log('user$:', newUser);
Lukas Burgey's avatar
Lukas Burgey committed
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

        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([]);
            this.sshKeys = [];
            this.sshKeys$.next([]);
          }


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

87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
          if (newUser.id) {
            this.connectLiveUpdates(newUser.id);
          }
          if (newUser.ssh_keys) {
            this.sshKeys = newUser.ssh_keys;
            this.sshKeys$.next(this.sshKeys);
          }
          if (newUser.deployments) {
            newUser.deployments.forEach(
              (newDep: Deployment) => {
                this.deployments.set(newDep.id,  newDep);
              }
            );
            this.deployments$.next(Array.from(this.deployments.values()));
          }

103
104
105
106
107
108
109
          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
110
          this.loggedIn = true;
111
112
113
          this.initialized = true;
        }
      },
Lukas Burgey's avatar
Lukas Burgey committed
114
115
      this.handleError(true),
      () => console.log('user$ is complete'),
116
    );
Lukas Burgey's avatar
Lukas Burgey committed
117
118
  }

119
  private connectLiveUpdates(userID: number): void {
Lukas Burgey's avatar
Lukas Burgey committed
120
    // handle with care
121
122
    const login = userID
    const passcode = this.cookieService.get('sessionid');
Lukas Burgey's avatar
Lukas Burgey committed
123
124
    const stompConfig: StompConfig = {
      // Which server?
125
      url: 'wss://'+window.location.host+'/ws',
126

Lukas Burgey's avatar
Lukas Burgey committed
127
128
129
130
131
132
      // Headers
      // Typical keys: login, passcode, host
      headers: {
        login: 'webpage-client:' + login,
        passcode: passcode,
      },
Lukas Burgey's avatar
Lukas Burgey committed
133

Lukas Burgey's avatar
Lukas Burgey committed
134
135
136
137
138
139
      // 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
140
141
      // Typical value 15000 (15 seconds)
      reconnect_delay: 15000,
142

Lukas Burgey's avatar
Lukas Burgey committed
143
144
145
      // Will log diagnostics on console
      debug: false,
    };
146

Lukas Burgey's avatar
Lukas Burgey committed
147
148
    this.stompService.config = stompConfig;
    this.stompService.initAndConnect();
Lukas Burgey's avatar
Lukas Burgey committed
149

150
    const subscription = this.stompService.subscribe(
151
      '/exchange/users/' + userID.toString()
Lukas Burgey's avatar
Lukas Burgey committed
152
    );
153

Lukas Burgey's avatar
Lukas Burgey committed
154
155
    subscription.subscribe(
      (message: Message) => {
156
157
158
        let update: Update = JSON.parse(message.body);
        console.log('update:', update);

Lukas Burgey's avatar
Lukas Burgey committed
159
        // TODO rename error to msg
Lukas Burgey's avatar
Lukas Burgey committed
160
161
        if (update.error && update.error != '') {
          this.snackBar.open(update.error);
Lukas Burgey's avatar
Lukas Burgey committed
162
        }
163

164
165
166
167
        if (update.deployment_state) {
          this.updateDeploymentState(update.deployment_state);
        }

168
169
        if (update.deployment) {
          this.updateDeployment(update.deployment);
Lukas Burgey's avatar
Lukas Burgey committed
170
        }
Lukas Burgey's avatar
Lukas Burgey committed
171
      },
172
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
173
    );
Lukas Burgey's avatar
Lukas Burgey committed
174
  }
175

Lukas Burgey's avatar
Lukas Burgey committed
176
  private updateDeployment(dep: Deployment): void {
177
178
179
    if (dep != undefined && dep != null) {
      dep.states.forEach((state: DeploymentState) => this.updateDeploymentState(state),
      );
Lukas Burgey's avatar
Lukas Burgey committed
180
181
182
183
      this.deployments.set(dep.id, dep);
      this.deployments$.next(Array.from(this.deployments.values()));
    }
  }
Lukas Burgey's avatar
Lukas Burgey committed
184

185
186
187
188
189
190
191
  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
192
  private updateState(update: State): void {
193
194
    if (update) {
      // report an occured error
Lukas Burgey's avatar
Lukas Burgey committed
195
196
197
198
199
      if (update.msg) {
        this.snackBar.open(update.msg);
      }
      if (update.session && update.session.deactivated) {
        this.autoReLogin = false;
200
      }
Lukas Burgey's avatar
Lukas Burgey committed
201

Lukas Burgey's avatar
Lukas Burgey committed
202
203
      console.log(update.session);

Lukas Burgey's avatar
Lukas Burgey committed
204
205
      this.user = update.user;
      this.user$.next(this.user);
206
207

    } else {
Lukas Burgey's avatar
Lukas Burgey committed
208
      this.user$.next(undefined);
Lukas Burgey's avatar
Lukas Burgey committed
209
210
211
    }
  }

212
  private fetch(): void {
Lukas Burgey's avatar
Lukas Burgey committed
213
    this.http.get<State>('/backend/api/state').subscribe(
214
      (state: State) => this.updateState(state),
Lukas Burgey's avatar
Lukas Burgey committed
215
      this.handleError(false, 'Error fetching state. Try again later'),
Lukas Burgey's avatar
Lukas Burgey committed
216
217
218
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
219
220
  private handleError(fetch: boolean, msg?: string) {
    return (error: any) => {
Lukas Burgey's avatar
Lukas Burgey committed
221
      if (error.status === 403 && this.autoReLogin) {
Lukas Burgey's avatar
Lukas Burgey committed
222
223
224
        this.login();
        return
      }
225

Lukas Burgey's avatar
Lukas Burgey committed
226
227
228
229
230
231
232
      if (msg) {
        this.snackBar.open(msg);
      }
      console.log('fetch:', error);
      if (fetch) {
        this.fetch();
      }
233
      return EMPTY
Lukas Burgey's avatar
Lukas Burgey committed
234
    }
Lukas Burgey's avatar
Lukas Burgey committed
235
236
  }

Lukas Burgey's avatar
Lukas Burgey committed
237
  private logErrorAndFetch(error: any)  {
Lukas Burgey's avatar
Lukas Burgey committed
238
    if (error.status === 403 && this.autoReLogin) {
239
      this.login();
Lukas Burgey's avatar
Lukas Burgey committed
240
    }
241
242
243
244

    console.log(error);
    this.snackBar.open('Error');
    this.fetch();
Lukas Burgey's avatar
Lukas Burgey committed
245
    return observableThrowError(error);
Lukas Burgey's avatar
Lukas Burgey committed
246
247
  }

Lukas Burgey's avatar
Lukas Burgey committed
248
  // PUBLIC API
249
250
251
252
  public login(idp?: IdP): void {
    if (idp) {
      this.idpService.setIdPPreference(idp);
    }
253

254
    window.location.href = '/backend/auth/v1/request';
255
256
  }

Lukas Burgey's avatar
Lukas Burgey committed
257
  public logout(): void {
Lukas Burgey's avatar
Lukas Burgey committed
258
    this.http.post<State>('/backend/auth/v1/logout', undefined).subscribe(
259
      (state: State) => this.updateState(state),
Lukas Burgey's avatar
Lukas Burgey committed
260
261
262
      (err: HttpErrorResponse) => {
        this.updateState(undefined);
      }
263
264
265
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
266
  public sentQuestionnaire(stateItemID: number, answers: JSONObject) {
Lukas Burgey's avatar
Lukas Burgey committed
267
268
269
    return this.http.patch<DeploymentState>(
      `/rest/dep-state?id=${ stateItemID }`,
      {
Lukas Burgey's avatar
Lukas Burgey committed
270
        'answers': answers,
Lukas Burgey's avatar
Lukas Burgey committed
271
272
      },
  ).pipe(
Lukas Burgey's avatar
Lukas Burgey committed
273
      catchError(this.handleError(true, "Error submitting answers")),
274
    ).subscribe(
Lukas Burgey's avatar
Lukas Burgey committed
275
      (state: DeploymentState) => this.updateDeploymentState(state),
276
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
277
278
279
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
280
  public deleteUser() {
281
    return this.http.delete('/rest/user').pipe(
282
283
      catchError(this.handleError(true, "Error deleting user")),
    ).subscribe(
284
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
285
          this.user$.next(undefined);
Lukas Burgey's avatar
Lukas Burgey committed
286
          this.snackBar.open('Deleted user from server');
Lukas Burgey's avatar
Lukas Burgey committed
287
      },
288
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
289
290
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
291

292
293
  public uploadSshKey(name: string, key: string | ArrayBuffer): void {
    this.http.post<SSHKey>('/rest/ssh-keys', {'name': name, 'key': key}).pipe(
294
295
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
296
      (newKey: SSHKey) => {
Lukas Burgey's avatar
Lukas Burgey committed
297
298
        this.sshKeys.push(newKey);
        this.sshKeys$.next(this.sshKeys);
299
      },
300
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
301
302
303
    );
  }

304
  public removeSshKey(key: SSHKey) {
Lukas Burgey's avatar
Lukas Burgey committed
305
    console.log('Deleting key:', key);
306
    return this.http.delete('/rest/ssh-key?id='+key.id.toString()).pipe(
307
308
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
309
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
310
311
312
313
          this.sshKeys = this.sshKeys.filter(
            k => key.id != k.id
          );
          this.sshKeys$.next(this.sshKeys);
314
      },
315
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
316
317
    );
  }
318

Lukas Burgey's avatar
Lukas Burgey committed
319
  public changeVODeployment(action: string, vo: VO): void {
320
321
    const body = {
      'type': action,
322
      'vo': vo.id,
323
    };
324
    this.http.post<Deployment>('/backend/api/deployments', body).pipe(
325
      catchError(this.handleError(true, "Error changing deployment")),
326
327
    ).subscribe(
      (dep: Deployment) => this.updateDeployment(dep),
328
329
330
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
331
332
333
334
335
336
  public changeServiceDeployment(action: string, service: Service): void {
    const body = {
      'type': action,
      'service': service.id,
    };
    this.http.post<Deployment>('/backend/api/deployments', body).pipe(
337
      catchError(this.handleError(true, "Error changing deployment")),
Lukas Burgey's avatar
Lukas Burgey committed
338
339
340
341
342
    ).subscribe(
      (dep: Deployment) => this.updateDeployment(dep),
    );
  }

343
344
  // DATA SERVICE API
  //
Lukas Burgey's avatar
Lukas Burgey committed
345
  public subscribeSpecific<T>(selector: (user: User) => T): Observable<T> {
346
    return this.subscribeUser().pipe(map(selector));
347
348
  }

Lukas Burgey's avatar
Lukas Burgey committed
349
350
  public subscribeSSHKeys(): Observable<SSHKey[]> {
    return this.sshKeys$.asObservable();
351
352
  }

353
354
  public subscribeUser(): Observable<User> {
    return this.user$.asObservable();
355
356
  }

357
358
  public subscribeDeployments(): Observable<Deployment[]> {
    return this.deployments$.asObservable();
359
360
  }

361
362
363
364
  public subscribeDeploymentStates(): Observable<DeploymentState[]> {
    return this.deploymentStates$.asObservable();
  }

Lukas Burgey's avatar
Lukas Burgey committed
365
  public subscribeStateFor(service: Service): Observable<DeploymentState> {
366
367
    return this.deploymentStates$.asObservable().pipe(
      map((states: DeploymentState[]) => states.find(
Lukas Burgey's avatar
Lukas Burgey committed
368
          (dsi: DeploymentState) => dsi.service.id == service.id,
369
370
371
372
      )),
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
373
  public subscribeDeployment(selector: (dep: Deployment) => boolean): Observable<Deployment> {
374
    return this.subscribeDeployments().pipe(
Lukas Burgey's avatar
Lukas Burgey committed
375
      map((deployments: Deployment[]) => {
376
          return deployments.find(
Lukas Burgey's avatar
Lukas Burgey committed
377
            (dep: Deployment) => selector(dep),
378
379
          );
        }
380
      ),
381
382
383
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
384
385
  public subscribeServices(): Observable<Service[]> {
    return this.subscribeSpecific(this.serviceSelector);
386
  }
Lukas Burgey's avatar
Lukas Burgey committed
387
388
389
390

  public subscribeVOs(): Observable<VO[]> {
    return this.prefs.connect().pipe(
      combineLatest(
Lukas Burgey's avatar
Lukas Burgey committed
391
392
393
394
395
        this.user$.asObservable(),
        (prefs: Prefs, user: User) => {
          const vos = this.voSelector(user);
          const services = this.serviceSelector(user);

Lukas Burgey's avatar
Lukas Burgey committed
396
          if (prefs.showEmptyVOs) {
Lukas Burgey's avatar
Lukas Burgey committed
397
            // filter out VOs that have no services
Lukas Burgey's avatar
Lukas Burgey committed
398
            return vos.filter(
Lukas Burgey's avatar
Lukas Burgey committed
399
              (vo: VO) => services.some(
Lukas Burgey's avatar
Lukas Burgey committed
400
401
402
403
404
405
                (s: Service) => s.vos.some(
                  svo => svo.id === vo.id,
                ),
              ),
            );
          }
Lukas Burgey's avatar
Lukas Burgey committed
406

Lukas Burgey's avatar
Lukas Burgey committed
407
408
409
410
411
          return vos;
        }
      )
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
412
}