user.service.ts 12.8 KB
Newer Older
Lukas Burgey's avatar
Lukas Burgey committed
1 2
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
3

Lukas Burgey's avatar
Lukas Burgey committed
4
import { throwError as observableThrowError,  Observable, BehaviorSubject, of, EMPTY } from 'rxjs';
5
import { map, catchError, combineLatest, tap } from 'rxjs/operators';
6

Lukas Burgey's avatar
Lukas Burgey committed
7 8 9 10 11 12
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
13
import { PreferencesService, Prefs } from './preferences/preferences.service';
Lukas Burgey's avatar
Lukas Burgey committed
14

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

19 20 21 22
export interface Combination {
  user: User;
  prefs: Prefs;
}
Lukas Burgey's avatar
Lukas Burgey committed
23 24 25

@Injectable()
export class UserService {
26
  private initialized = false;
Lukas Burgey's avatar
Lukas Burgey committed
27 28
  private loggedIn = false;

29
  private observerDebugging = true;
30

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

35
  private user: User;
36 37
  private user$: BehaviorSubject<User> = new BehaviorSubject(undefined);
  private userEmissions: number = 0;
Lukas Burgey's avatar
Lukas Burgey committed
38

Lukas Burgey's avatar
Lukas Burgey committed
39
  private sshKeys: Map<number, SSHKey> = new Map([]);
Lukas Burgey's avatar
Lukas Burgey committed
40
  private sshKeys$ = new BehaviorSubject<SSHKey[]>([]);
Lukas Burgey's avatar
Lukas Burgey committed
41

42 43 44
  private deploymentStates: Map<number, DeploymentState> = new Map([]);
  private deploymentStates$ = new BehaviorSubject<DeploymentState[]>([]);

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

48
  public voSelector = (user: User) => user ? user.vos : [];
Lukas Burgey's avatar
Lukas Burgey committed
49
  public serviceSelector = (user: User) => user ? user.services : [];
Lukas Burgey's avatar
Lukas Burgey committed
50 51

  constructor(
52 53 54 55
    private cookieService: CookieService,
    private http: HttpClient,
    private snackBar: SnackBarService,
    private idpService: IdpService,
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 65
    this.fetch();

Lukas Burgey's avatar
Lukas Burgey committed
66
    this.userSrc().subscribe(
67
      (newUser: User) => {
Lukas Burgey's avatar
Lukas Burgey committed
68 69 70 71 72 73 74 75 76 77 78
        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([]);
Lukas Burgey's avatar
Lukas Burgey committed
79
            this.sshKeys = new Map([]);
Lukas Burgey's avatar
Lukas Burgey committed
80 81 82 83 84 85 86 87 88 89 90
            this.sshKeys$.next([]);
          }

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

91 92 93 94
          if (newUser.id) {
            this.connectLiveUpdates(newUser.id);
          }
          if (newUser.ssh_keys) {
Lukas Burgey's avatar
Lukas Burgey committed
95 96 97 98 99
            this.sshKeys = new Map([]);
            newUser.ssh_keys.forEach(
              (key: SSHKey) => this.sshKeys.set(key.id, key),
            );
            this.sshKeys$.next(Array.from(this.sshKeys.values()));
100 101 102 103 104 105 106 107 108 109
          }
          if (newUser.deployments) {
            newUser.deployments.forEach(
              (newDep: Deployment) => {
                this.deployments.set(newDep.id,  newDep);
              }
            );
            this.deployments$.next(Array.from(this.deployments.values()));
          }

110 111 112 113 114 115 116
          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
117
          this.loggedIn = true;
118 119 120
          this.initialized = true;
        }
      },
Lukas Burgey's avatar
Lukas Burgey committed
121 122
      this.handleError(true),
      () => console.log('user$ is complete'),
123
    );
124 125
  }

126
  private connectLiveUpdates(userID: number): void {
Lukas Burgey's avatar
Lukas Burgey committed
127
    // handle with care
128 129
    const login = userID
    const passcode = this.cookieService.get('sessionid');
Lukas Burgey's avatar
Lukas Burgey committed
130 131
    const stompConfig: StompConfig = {
      // Which server?
132
      url: 'wss://'+window.location.host+'/ws',
133

Lukas Burgey's avatar
Lukas Burgey committed
134 135 136 137 138 139
      // Headers
      // Typical keys: login, passcode, host
      headers: {
        login: 'webpage-client:' + login,
        passcode: passcode,
      },
Lukas Burgey's avatar
Lukas Burgey committed
140

Lukas Burgey's avatar
Lukas Burgey committed
141 142 143 144 145 146
      // 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
147 148
      // Typical value 15000 (15 seconds)
      reconnect_delay: 15000,
149

Lukas Burgey's avatar
Lukas Burgey committed
150 151 152
      // Will log diagnostics on console
      debug: false,
    };
153

Lukas Burgey's avatar
Lukas Burgey committed
154 155
    this.stompService.config = stompConfig;
    this.stompService.initAndConnect();
Lukas Burgey's avatar
Lukas Burgey committed
156

157
    const subscription = this.stompService.subscribe(
158
      '/exchange/users/' + userID.toString()
Lukas Burgey's avatar
Lukas Burgey committed
159
    );
160

161 162
    subscription.subscribe(
      (message: Message) => {
163 164 165
        let update: Update = JSON.parse(message.body);
        console.log('update:', update);

Lukas Burgey's avatar
Lukas Burgey committed
166
        // TODO rename error to msg
167 168
        if (update.error && update.error != '') {
          this.snackBar.open(update.error);
Lukas Burgey's avatar
Lukas Burgey committed
169
        }
170

171 172 173 174
        if (update.deployment_state) {
          this.updateDeploymentState(update.deployment_state);
        }

175 176
        if (update.deployment) {
          this.updateDeployment(update.deployment);
Lukas Burgey's avatar
Lukas Burgey committed
177
        }
178
      },
179
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
180
    );
Lukas Burgey's avatar
Lukas Burgey committed
181
  }
182

Lukas Burgey's avatar
Lukas Burgey committed
183
  private updateDeployment(dep: Deployment): void {
184 185 186
    if (dep != undefined && dep != null) {
      dep.states.forEach((state: DeploymentState) => this.updateDeploymentState(state),
      );
Lukas Burgey's avatar
Lukas Burgey committed
187 188 189 190
      this.deployments.set(dep.id, dep);
      this.deployments$.next(Array.from(this.deployments.values()));
    }
  }
Lukas Burgey's avatar
Lukas Burgey committed
191

192 193 194 195 196 197 198
  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
199
  private updateState(update: State): void {
200 201
    if (update) {
      // report an occured error
Lukas Burgey's avatar
Lukas Burgey committed
202 203 204 205 206
      if (update.msg) {
        this.snackBar.open(update.msg);
      }
      if (update.session && update.session.deactivated) {
        this.autoReLogin = false;
207
      }
Lukas Burgey's avatar
Lukas Burgey committed
208

Lukas Burgey's avatar
Lukas Burgey committed
209 210
      console.log(update.session);

Lukas Burgey's avatar
Lukas Burgey committed
211 212
      this.user = update.user;
      this.user$.next(this.user);
213 214

    } else {
Lukas Burgey's avatar
Lukas Burgey committed
215
      this.user$.next(undefined);
216 217 218
    }
  }

219
  private fetch(): void {
Lukas Burgey's avatar
Lukas Burgey committed
220
    this.http.get<State>('/backend/api/state').subscribe(
221
      (state: State) => this.updateState(state),
Lukas Burgey's avatar
Lukas Burgey committed
222
      this.handleError(false, 'Error fetching state. Try again later'),
223 224 225
    );
  }

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

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

Lukas Burgey's avatar
Lukas Burgey committed
244
  private logErrorAndFetch(error: any)  {
Lukas Burgey's avatar
Lukas Burgey committed
245
    if (error.status === 403 && this.autoReLogin) {
246
      this.login();
247
    }
248 249 250 251

    console.log(error);
    this.snackBar.open('Error');
    this.fetch();
Lukas Burgey's avatar
Lukas Burgey committed
252
    return observableThrowError(error);
Lukas Burgey's avatar
Lukas Burgey committed
253 254
  }

255 256 257 258 259 260 261 262
  private tapLogger(name: string): (e: any) => void {
    return e => {
      if (this.observerDebugging) {
        console.log(name, e);
      }
    }
  }

Lukas Burgey's avatar
Lukas Burgey committed
263
  // PUBLIC API
264 265 266 267
  public login(idp?: IdP): void {
    if (idp) {
      this.idpService.setIdPPreference(idp);
    }
268

269
    window.location.href = '/backend/auth/v1/request';
270 271
  }

Lukas Burgey's avatar
Lukas Burgey committed
272
  public logout(): void {
Lukas Burgey's avatar
Lukas Burgey committed
273
    this.http.post<State>('/backend/auth/v1/logout', undefined).subscribe(
274
      (state: State) => this.updateState(state),
Lukas Burgey's avatar
Lukas Burgey committed
275 276 277
      (err: HttpErrorResponse) => {
        this.updateState(undefined);
      }
278 279 280
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
281
  public sentQuestionnaire(stateItemID: number, answers: JSONObject) {
282 283 284
    return this.http.patch<DeploymentState>(
      `/rest/dep-state?id=${ stateItemID }`,
      {
Lukas Burgey's avatar
Lukas Burgey committed
285
        'answers': answers,
286 287
      },
  ).pipe(
Lukas Burgey's avatar
Lukas Burgey committed
288
      catchError(this.handleError(true, "Error submitting answers")),
289
    ).subscribe(
290
      (state: DeploymentState) => this.updateDeploymentState(state),
291
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
292 293 294
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
295
  public deleteUser() {
296
    return this.http.delete('/rest/user').pipe(
297 298
      catchError(this.handleError(true, "Error deleting user")),
    ).subscribe(
299
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
300
          this.user$.next(undefined);
Lukas Burgey's avatar
Lukas Burgey committed
301
          this.snackBar.open('Deleted user from server');
Lukas Burgey's avatar
Lukas Burgey committed
302
      },
303
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
304 305
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
306

307 308
  public uploadSshKey(name: string, key: string | ArrayBuffer): void {
    this.http.post<SSHKey>('/rest/ssh-keys', {'name': name, 'key': key}).pipe(
309 310
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
311
      (newKey: SSHKey) => {
Lukas Burgey's avatar
Lukas Burgey committed
312 313
        this.sshKeys.set(newKey.id, newKey);
        this.sshKeys$.next(Array.from(this.sshKeys.values()));
314
      },
315
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
316 317 318
    );
  }

319
  public removeSshKey(key: SSHKey) {
Lukas Burgey's avatar
Lukas Burgey committed
320
    console.log('Deleting key:', key);
321
    return this.http.delete('/rest/ssh-key?id='+key.id.toString()).pipe(
322 323
      catchError(this.handleError(true, "Error changing deployment")),
    ).subscribe(
324
      _ => {
Lukas Burgey's avatar
Lukas Burgey committed
325 326
        this.sshKeys.delete(key.id);
        this.sshKeys$.next(Array.from(this.sshKeys.values()));
327
      },
328
      this.logErrorAndFetch,
Lukas Burgey's avatar
Lukas Burgey committed
329 330
    );
  }
331

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

Lukas Burgey's avatar
Lukas Burgey committed
344 345 346 347 348 349
  public changeServiceDeployment(action: string, service: Service): void {
    const body = {
      'type': action,
      'service': service.id,
    };
    this.http.post<Deployment>('/backend/api/deployments', body).pipe(
350
      catchError(this.handleError(true, "Error changing deployment")),
Lukas Burgey's avatar
Lukas Burgey committed
351 352 353 354 355
    ).subscribe(
      (dep: Deployment) => this.updateDeployment(dep),
    );
  }

356 357
  // DATA SERVICE API
  //
Lukas Burgey's avatar
Lukas Burgey committed
358
  public subscribeSpecific<T>(selector: (user: User) => T): Observable<T> {
359 360 361 362
    return this.userSrc().pipe(
      map(selector),
      tap(this.tapLogger('subscribeSpecific')),
    );
363 364
  }

Lukas Burgey's avatar
Lukas Burgey committed
365
  public sshKeysSrc(): Observable<SSHKey[]> {
366 367 368
    return this.sshKeys$.asObservable().pipe(
      tap(this.tapLogger('sshKeysSrc')),
    );
369 370
  }

Lukas Burgey's avatar
Lukas Burgey committed
371
  public userSrc(): Observable<User> {
372 373 374
    return this.user$.asObservable().pipe(
      tap(this.tapLogger('userSrc')),
    );
375 376
  }

377 378 379 380 381 382 383 384 385 386 387 388
  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
389
  public deploymentsSrc(): Observable<Deployment[]> {
390 391 392
    return this.deployments$.asObservable().pipe(
      tap(this.tapLogger('deploymentsSrc')),
    );
393 394
  }

Lukas Burgey's avatar
Lukas Burgey committed
395
  public depStatesSrc(): Observable<DeploymentState[]> {
396 397 398
    return this.deploymentStates$.asObservable().pipe(
      tap(this.tapLogger('depStatesSrc')),
    );
399 400
  }

Lukas Burgey's avatar
Lukas Burgey committed
401
  public subscribeStateFor(service: Service): Observable<DeploymentState> {
402 403
    return this.deploymentStates$.asObservable().pipe(
      map((states: DeploymentState[]) => states.find(
Lukas Burgey's avatar
Lukas Burgey committed
404
          (dsi: DeploymentState) => dsi.service.id == service.id,
405
      )),
406
      tap(this.tapLogger('subscribeStateFor')),
407 408 409
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
410
  public subscribeDeployment(selector: (dep: Deployment) => boolean): Observable<Deployment> {
Lukas Burgey's avatar
Lukas Burgey committed
411
    return this.deploymentsSrc().pipe(
Lukas Burgey's avatar
Lukas Burgey committed
412
      map((deployments: Deployment[]) => {
413
          return deployments.find(
Lukas Burgey's avatar
Lukas Burgey committed
414
            (dep: Deployment) => selector(dep),
415 416
          );
        }
417
      ),
418
      tap(this.tapLogger('subscribeDeployment')),
419 420 421
    );
  }

Lukas Burgey's avatar
Lukas Burgey committed
422
  public servicesSrc(): Observable<Service[]> {
423 424 425
    return this.subscribeSpecific(this.serviceSelector).pipe(
      tap(this.tapLogger('servicesSrc')),
    );
426
  }
Lukas Burgey's avatar
Lukas Burgey committed
427

428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
  public extractVOs(combi: Combination): VO[] {
    if (combi.prefs.showEmptyVOs) {
      // 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,
          ),
        ),
      );
    }

    return combi.user.vos;

  }

Lukas Burgey's avatar
Lukas Burgey committed
444
  public vosSrc(): Observable<VO[]> {
Lukas Burgey's avatar
Lukas Burgey committed
445 446
    return this.prefs.connect().pipe(
      combineLatest(
Lukas Burgey's avatar
Lukas Burgey committed
447 448 449 450 451
        this.user$.asObservable(),
        (prefs: Prefs, user: User) => {
          const vos = this.voSelector(user);
          const services = this.serviceSelector(user);

Lukas Burgey's avatar
Lukas Burgey committed
452
          if (prefs.showEmptyVOs) {
Lukas Burgey's avatar
Lukas Burgey committed
453
            // filter out VOs that have no services
Lukas Burgey's avatar
Lukas Burgey committed
454
            return vos.filter(
Lukas Burgey's avatar
Lukas Burgey committed
455
              (vo: VO) => services.some(
Lukas Burgey's avatar
Lukas Burgey committed
456 457 458 459 460 461
                (s: Service) => s.vos.some(
                  svo => svo.id === vo.id,
                ),
              ),
            );
          }
Lukas Burgey's avatar
Lukas Burgey committed
462

Lukas Burgey's avatar
Lukas Burgey committed
463 464
          return vos;
        }
465
      ),
Lukas Burgey's avatar
Lukas Burgey committed
466 467
    );
  }
Lukas Burgey's avatar
Lukas Burgey committed
468
}