cloudfoundry/stratos

View on GitHub
src/frontend/packages/store/src/effects/endpoint.effects.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { catchError, mergeMap } from 'rxjs/operators';

import {
  CONNECT_ENDPOINTS,
  ConnectEndpoint,
  DISCONNECT_ENDPOINTS,
  DisconnectEndpoint,
  EndpointActionComplete,
  GET_ENDPOINT,
  GET_ENDPOINTS,
  GetAllEndpoints,
  GetAllEndpointsSuccess,
  GetEndpoint,
  REGISTER_ENDPOINTS,
  RegisterEndpoint,
  UNREGISTER_ENDPOINTS,
  UnregisterEndpoint,
} from '../actions/endpoint.actions';
import { SendClearEventAction } from '../actions/internal-events.actions';
import { ClearPaginationOfEntity } from '../actions/pagination.actions';
import { GET_SYSTEM_INFO_SUCCESS, GetSystemSuccess } from '../actions/system.actions';
import { DispatchOnlyAppState } from '../app-state';
import { BrowserStandardEncoder } from '../browser-encoder';
import { entityCatalog } from '../entity-catalog/entity-catalog';
import { EndpointType } from '../extension-types';
import { httpErrorResponseToSafeString } from '../jetstream';
import { ApiRequestTypes } from '../reducers/api-request-reducer/request-helpers';
import { stratosEntityCatalog } from '../stratos-entity-catalog';
import { NormalizedResponse } from '../types/api.types';
import { EndpointModel } from '../types/endpoint.types';
import {
  EntityRequestAction,
  StartRequestAction,
  WrapperRequestActionFailed,
  WrapperRequestActionSuccess,
} from '../types/request.types';
import { UPDATE_ENDPOINT, UpdateEndpoint } from './../actions/endpoint.actions';
import { PaginatedAction } from './../types/pagination.types';


@Injectable()
export class EndpointsEffect {

  constructor(
    private http: HttpClient,
    private actions$: Actions,
    private store: Store<DispatchOnlyAppState>
  ) { }

  @Effect() getEndpoint$ = this.actions$.pipe(
    ofType<GetEndpoint>(GET_ENDPOINT),
    mergeMap((action: GetEndpoint) => [
      stratosEntityCatalog.systemInfo.actions.getSystemInfo(false, action)
    ])
  );

  @Effect() getAllEndpointsBySystemInfo$ = this.actions$.pipe(
    ofType<GetAllEndpoints>(GET_ENDPOINTS),
    mergeMap((action: GetAllEndpoints) => [
      stratosEntityCatalog.systemInfo.actions.getSystemInfo(false, action)
    ])
  );

  @Effect() getAllEndpoints$ = this.actions$.pipe(
    ofType<GetSystemSuccess>(GET_SYSTEM_INFO_SUCCESS),
    mergeMap(action => {
      const { associatedAction } = action;
      const entityKey = entityCatalog.getEntityKey(associatedAction);
      const endpoints = action.payload.endpoints;
      // Data is an array of endpoints
      const mappedData: NormalizedResponse<EndpointModel> = {
        entities: {
          [entityKey]: {}
        },
        result: []
      };

      Object.keys(endpoints).forEach((type: string) => {
        const endpointsForType = endpoints[type];
        Object.values(endpointsForType).forEach(endpointInfo => {
          mappedData.entities[entityKey][endpointInfo.guid] = {
            ...endpointInfo,
            connectionStatus: endpointInfo.user ? 'connected' : 'disconnected',
          };
          mappedData.result.push(endpointInfo.guid);
        });
      });

      const isLogin = associatedAction.type === GET_ENDPOINTS ? (associatedAction as GetAllEndpoints).login : false;

      // Order is important. Need to ensure data is written (none cf action success) before we notify everything is loaded
      // (endpoint success)
      return [
        new WrapperRequestActionSuccess(mappedData, associatedAction, 'fetch'),
        new GetAllEndpointsSuccess(mappedData, isLogin),
      ];
    }));

  @Effect() connectEndpoint$ = this.actions$.pipe(
    ofType<ConnectEndpoint>(CONNECT_ENDPOINTS),
    mergeMap(action => {
      // Special-case SSO login - redirect to the back-end
      if (action.authType === 'sso') {
        const loc = window.location.protocol + '//' + window.location.hostname +
          (window.location.port ? ':' + window.location.port : '');
        const ssoUrl = '/api/v1/tokens?guid=' + action.guid + '&state=' + encodeURIComponent(loc);
        window.location.assign(ssoUrl);
        return [];
      }

      let fromObject: any;
      let body = action.body as any;

      if (action.body) {
        fromObject = {
          ...action.authValues,
          cnsi_guid: action.guid,
          connect_type: action.authType,
          system_shared: action.systemShared
        };
      } else {
        // If no body, then we will put the auth values in the body, not in the URL
        fromObject = {
          cnsi_guid: action.guid,
          connect_type: action.authType,
          system_shared: action.systemShared
        };

        // Encode auth values in the body
        body = new FormData();
        Object.keys(action.authValues).forEach(key => {
          body.set(key, action.authValues[key]);
        });
      }

      const params: HttpParams = new HttpParams({
        fromObject,
        encoder: new BrowserStandardEncoder()
      });

      return this.doEndpointAction(
        action,
        '/api/v1/tokens',
        params,
        null,
        action.endpointsType,
        body,
        response => httpErrorResponseToSafeString(response) || 'Could not connect, please try again',
      );
    }));

  @Effect() disconnect$ = this.actions$.pipe(
    ofType<DisconnectEndpoint>(DISCONNECT_ENDPOINTS),
    mergeMap(action => {

      return this.doEndpointAction(
        action,
        '/api/v1/tokens/' + action.guid,
        null,
        null,
        action.endpointsType,
        null,
        null,
        'DELETE'
      );
    }));

  @Effect() unregister$ = this.actions$.pipe(
    ofType<UnregisterEndpoint>(UNREGISTER_ENDPOINTS),
    mergeMap(action => {
      return this.doEndpointAction(
        action,
        '/api/v1/endpoints/' + action.guid,
        null,
        'delete',
        action.endpointsType,
        null,
        null,
        'DELETE'
      );
    }));

  @Effect() register$ = this.actions$.pipe(
    ofType<RegisterEndpoint>(REGISTER_ENDPOINTS),
    mergeMap(action => {

      const paramsObj = {
        cnsi_name: action.name,
        api_endpoint: action.endpoint,
        skip_ssl_validation: action.skipSslValidation ? 'true' : 'false',
        cnsi_client_id: action.clientID,
        cnsi_client_secret: action.clientSecret,
        sso_allowed: action.ssoAllowed ? 'true' : 'false',
        create_system_endpoint: action.createSystemEndpoint ? 'true' : 'false'
      };
      // Do not include sub_type in HttpParams if it doesn't exist (falsies get stringified and sent)
      if (action.endpointSubType) {
        /* tslint:disable-next-line:no-string-literal  */
        paramsObj['sub_type'] = action.endpointSubType;
      }
      // Encode auth values in the body, not the query string
      const body: any = new FormData();
      Object.keys(paramsObj).forEach(key => {
        body.set(key, paramsObj[key]);
      });

      return this.doEndpointAction(
        action,
        '/api/v1/endpoints',
        new HttpParams({
          fromObject: {
            endpoint_type: action.endpointsType
          }
        }),
        'create',
        action.endpointsType,
        body,
        this.processRegisterError
      );
    }));

  @Effect() updateEndpoint$ = this.actions$.pipe(
    ofType<UpdateEndpoint>(UPDATE_ENDPOINT),
    mergeMap((action: UpdateEndpoint) => {
      const paramsObj = {
        name: action.name,
        skipSSL: action.skipSSL,
        setClientInfo: action.setClientInfo,
        clientID: action.clientID,
        clientSecret: action.clientSecret,
        allowSSO: action.allowSSO,
      };

      // Encode auth values in the body, not the query string
      const body: any = new FormData();
      Object.keys(paramsObj).forEach(key => {
        body.set(key, paramsObj[key]);
      });

      return this.doEndpointAction(
        action,
        '/api/v1/endpoints/' + action.id,
        new HttpParams({}),
        'update',
        action.endpointsType,
        body,
        this.processUpdateError
      );
    }));

  private processUpdateError(e: HttpErrorResponse): string {
    let message = 'There was a problem updating the endpoint. ' +
      httpErrorResponseToSafeString(e);
    if (e.status === 403) {
      message = `${message}. Please check \"Skip SSL validation for the endpoint\" if the certificate issuer is trusted`;
    }
    return message;
  }

  private processRegisterError(e: HttpErrorResponse): string {
    let message = 'There was a problem creating the endpoint. Please ensure the endpoint address is correct and try again. ' +
      httpErrorResponseToSafeString(e);
    if (e.status === 403) {
      message = `${e.error.error}. Please check \"Skip SSL validation for the endpoint\" if the certificate issuer is trusted`;
    }
    return message;
  }

  /**
   * @param endpointType The underlying endpoints type (_cf_Endpoint, not _stratos_Endpoint)
   */
  private doEndpointAction(
    apiAction: EntityRequestAction | PaginatedAction,
    url: string,
    params: HttpParams,
    apiActionType: ApiRequestTypes = 'update',
    endpointType: EndpointType,
    body?: string,
    errorMessageHandler?: (e: any) => string,
    method: string = 'POST',
  ) {

    const endpointEntityKey = entityCatalog.getEntityKey(apiAction);
    this.store.dispatch(new StartRequestAction(apiAction, apiActionType));
    return this.http.request(method, url, {
      params,
      body: body || {}
    }).pipe(
      mergeMap((endpoint: EndpointModel) => {
        const actions = [];
        let response: NormalizedResponse<EndpointModel>;
        if (apiAction.actions[1]) {
          actions.push(new EndpointActionComplete(apiAction.actions[1], apiAction.guid, endpointType, endpoint));
        }

        if (apiActionType === 'delete') {
          actions.push(new ClearPaginationOfEntity(apiAction, apiAction.guid));
          actions.push(stratosEntityCatalog.userFavorite.actions.getAll());
        }

        if (apiActionType === 'create') {
          actions.push(stratosEntityCatalog.systemInfo.actions.getSystemInfo());
          response = {
            entities: {
              [endpointEntityKey]: {
                [endpoint.guid]: endpoint
              }
            },
            result: [endpoint.guid]
          };
        }

        if (apiActionType === 'update') {
          actions.push(stratosEntityCatalog.systemInfo.actions.getSystemInfo());
        }

        if (apiAction.updatingKey === DisconnectEndpoint.UpdatingKey || apiActionType === 'create' || apiActionType === 'delete'
          || apiActionType === 'update') {
          actions.push(this.clearEndpointInternalEvents(apiAction.guid, endpointEntityKey));
        }

        actions.push(new WrapperRequestActionSuccess(response, apiAction, apiActionType, null, null, endpoint ? endpoint.guid : null));
        return actions;
      }
      ),
      catchError(e => {
        const actions = [];
        if (apiAction.actions[2]) {
          actions.push({ type: apiAction.actions[2], guid: apiAction.guid });
        }
        const errorMessage = errorMessageHandler ? errorMessageHandler(e) : 'Could not perform action';
        actions.push(new WrapperRequestActionFailed(errorMessage, apiAction, apiActionType));
        return actions;
      }));
  }

  private clearEndpointInternalEvents(guid: string, endpointEntityKey: string) {
    return new SendClearEventAction(
      endpointEntityKey,
      guid,
      {
        clean: true
      }
    );
  }
}