user.service.ts 11.1 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 {
14
  VO, User, Update, State, Deployment, DeploymentState, SSHKey, NewSSHKey, IdP, Service, Site
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
    );
Lukas Burgey's avatar
Lukas Burgey committed
215
216
217
218
219

    this.http.post<SSHKey>('/rest/ssh-keys', {'name': 'foo', 'key': 'bar'}).subscribe(
      (key: SSHKey) => console.log(key),
      err => console.log(err),
    );
Lukas Burgey's avatar
Lukas Burgey committed
220
221
  }

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

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

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

    console.log(error);
    this.snackBar.open('Error');
    this.fetch();
Lukas Burgey's avatar
Lukas Burgey committed
248
    return observableThrowError(error);
Lukas Burgey's avatar
Lukas Burgey committed
249
250
  }

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

257
    window.location.href = '/backend/auth/v1/request';
258
259
  }

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

269
  public sentQuestionnaire(stateItemID: number, answers: Object) {
270
271
272
    return this.http.post<Deployment>('/backend/api/questionnaire?id='+String(stateItemID), answers).pipe(
      catchError(this.handleError(true, "Error submitting questionnaire")),
    ).subscribe(
Lukas Burgey's avatar
Lukas Burgey committed
273
      (dep: Deployment) => this.updateDeployment(dep),
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

Lukas Burgey's avatar
Lukas Burgey committed
292
  public uploadSshKey(formData: FormData): void {
293
294
295
    this.http.post<SSHKey>('/backend/api/sshkey', formData).pipe(
      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);
Lukas Burgey's avatar
Lukas Burgey committed
306
307
308
    return this.http.post('/backend/api/sshkey', {
      'type': 'remove',
      'id': key.id,
309
310
311
    }).pipe(
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
Lukas Burgey's avatar
Lukas Burgey committed
312
313
314
315
316
317
318
      (data: {deleted: boolean}) => {
        if (data && data.deleted) {
          this.sshKeys = this.sshKeys.filter(
            k => key.id != k.id
          );
          this.sshKeys$.next(this.sshKeys);
        }
319
      },
320
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
321
322
    );
  }
323

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

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

348
349
  // DATA SERVICE API
  //
Lukas Burgey's avatar
Lukas Burgey committed
350
  public subscribeSpecific<T>(selector: (user: User) => T): Observable<T> {
351
    return this.subscribeUser().pipe(map(selector));
352
353
  }

Lukas Burgey's avatar
Lukas Burgey committed
354
355
  public subscribeSSHKeys(): Observable<SSHKey[]> {
    return this.sshKeys$.asObservable();
356
357
  }

358
359
  public subscribeUser(): Observable<User> {
    return this.user$.asObservable();
360
361
  }

362
363
  public subscribeDeployments(): Observable<Deployment[]> {
    return this.deployments$.asObservable();
364
365
  }

366
367
368
369
  public subscribeDeploymentStates(): Observable<DeploymentState[]> {
    return this.deploymentStates$.asObservable();
  }

Lukas Burgey's avatar
Lukas Burgey committed
370
  public subscribeStateFor(service: Service): Observable<DeploymentState> {
371
372
    return this.deploymentStates$.asObservable().pipe(
      map((states: DeploymentState[]) => states.find(
Lukas Burgey's avatar
Lukas Burgey committed
373
          (dsi: DeploymentState) => dsi.service.id == service.id,
374
375
376
377
      )),
    );
  }

378
379
380
  public subscribeVOs(): Observable<VO[]> {
    let voSelector = (user: User) => user ? user.vos : [];
    return this.subscribeSpecific<VO[]>(voSelector);
381
382
  }

Lukas Burgey's avatar
Lukas Burgey committed
383
  public subscribeDeployment(selector: (dep: Deployment) => boolean): Observable<Deployment> {
384
    return this.subscribeDeployments().pipe(
Lukas Burgey's avatar
Lukas Burgey committed
385
      map((deployments: Deployment[]) => {
386
          return deployments.find(
Lukas Burgey's avatar
Lukas Burgey committed
387
            (dep: Deployment) => selector(dep),
388
389
          );
        }
390
      ),
391
392
393
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
394
395
  public subscribeServices(): Observable<Service[]> {
    return this.subscribeSpecific(this.serviceSelector);
396
  }
Lukas Burgey's avatar
Lukas Burgey committed
397
}