user.service.ts 11 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';
4
import { map, catchError } 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

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
Lukas Burgey's avatar
Lukas Burgey committed
15
} from './types/types.module';
Lukas Burgey's avatar
Lukas Burgey committed
16

Lukas Burgey's avatar
Lukas Burgey committed
17
18
19

@Injectable()
export class UserService {
20
  private initialized = false;
Lukas Burgey's avatar
Lukas Burgey committed
21
22
23
24
25
  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
26

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

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

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

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

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

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

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

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

        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');
          }

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
          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()));
          }

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

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

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

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

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

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

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

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

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

162
163
164
165
        if (update.deployment_state) {
          this.updateDeploymentState(update.deployment_state);
        }

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

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

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

Lukas Burgey's avatar
Lukas Burgey committed
200
201
      console.log(update.session);

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

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

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

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

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

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

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

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

252
    window.location.href = '/backend/auth/v1/request';
253
254
  }

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

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

Lukas Burgey's avatar
Lukas Burgey committed
278
  public deleteUser() {
279
280
281
    return this.http.delete('/backend/api/delete_user').pipe(
      catchError(this.handleError(true, "Error deleting user")),
    ).subscribe(
Lukas Burgey's avatar
Lukas Burgey committed
282
283
      (data: {deleted: boolean}) => {
        if (data && data.deleted) {
Lukas Burgey's avatar
Lukas Burgey committed
284
          this.user$.next(undefined);
Lukas Burgey's avatar
Lukas Burgey committed
285
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
      )),
    );
  }

373
374
375
  public subscribeVOs(): Observable<VO[]> {
    let voSelector = (user: User) => user ? user.vos : [];
    return this.subscribeSpecific<VO[]>(voSelector);
376
377
  }

Lukas Burgey's avatar
Lukas Burgey committed
378
  public subscribeDeployment(selector: (dep: Deployment) => boolean): Observable<Deployment> {
379
    return this.subscribeDeployments().pipe(
Lukas Burgey's avatar
Lukas Burgey committed
380
      map((deployments: Deployment[]) => {
381
          return deployments.find(
Lukas Burgey's avatar
Lukas Burgey committed
382
            (dep: Deployment) => selector(dep),
383
384
          );
        }
385
      ),
386
387
388
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
389
390
  public subscribeServices(): Observable<Service[]> {
    return this.subscribeSpecific(this.serviceSelector);
391
  }
Lukas Burgey's avatar
Lukas Burgey committed
392
}