import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl, ValidatorFn, Validators } from '@angular/forms'; import { LookupService } from '@core/services/lookup.service'; import { SpinnerService } from '@core/services/spinner.service'; import { APIExternalAPI } from '@core/typings/api/external-api.typing'; import { CKEditorTokenConfig, SiblingValueValidator, TypeaheadSelectOption, TypeSafeFormArray, TypeSafeFormBuilder, TypeSafeFormGroup, URL_REGEX } from '@yourcause/common'; import { AnalyticsService, EventType } from '@yourcause/common/analytics'; import { I18nService } from '@yourcause/common/i18n'; import { YCModalComponent } from '@yourcause/common/modals'; @Component({ selector: 'gc-inbound-api-modal', templateUrl: './inbound-api-modal.component.html', styleUrls: ['./inbound-api-modal.component.scss'] }) export class InboundApiModalComponent extends YCModalComponent implements OnInit { @Input() service: APIExternalAPI.ExternalAPIPayload; @Input() modalType: 'edit'|'create'; postBodyConfig = { extraPlugins: 'token', availableTokens: [[ this.i18n.translate('common:lblFirstName', {}, 'First name'), 'FIRST_NAME' ], [ this.i18n.translate('common:lblLastName', {}, 'Last name'), 'LAST_NAME' ], [ this.i18n.translate('common:textEmail', {}, 'Email'), 'EMAIL' ], [ this.i18n.translate('common:lblSSOID', {}, 'SSO ID'), 'SSO_ID' ], [ this.i18n.translate('common:lblExternalSSOID', {}, 'External SSO ID'), 'EXTERNAL_SSO_ID' ], [ this.i18n.translate('common:lblFormField', {}, 'Form field'), 'FORM_FIELD' ], [ this.i18n.translate('GLOBAL:textExternalEmployeeId', {}, 'External employee ID'), 'EXTERNAL_EMPLOYEE_ID' ], [ this.i18n.translate('GLOBAL:textExternalEmployeeId2', {}, 'External employee ID 2'), 'EXTERNAL_EMPLOYEE_ID_2' ]] as [string, string][], tokenStart: '{{', tokenEnd: '}}', toolbar: [{ name: 'Tokens', items: ['CreateToken'] }] }; urlEditorConfig: CKEditorTokenConfig = { ...this.postBodyConfig, keystrokes: [ [ 13 /* Enter */, 'blur'], [ CKEDITOR.SHIFT + 13 /* Shift + Enter */, 'blur' ] ] }; alertMessage: string; modalReady = false; formGroup: TypeSafeFormGroup; responseFieldArray: TypeSafeFormArray; headerFormArray: TypeSafeFormArray; httpMethods: TypeaheadSelectOption[]; isPost = false; editField = false; responseFieldError = false; passwordVisible: boolean; ExternalAPIAuthScheme = APIExternalAPI.ExternalAPIAuthScheme; authenticationTypes: TypeaheadSelectOption[]; authenticationType: APIExternalAPI.ExternalAPIAuthScheme = null; formResponseFields: APIExternalAPI.ExternalParamFields[] = []; headers: APIExternalAPI.HeaderFields[] = []; editIndex: number = null; headerEditIndex: number = null; constructor ( private spinnerService: SpinnerService, private i18n: I18nService, private formBuilder: TypeSafeFormBuilder, private lookupService: LookupService, private analyticsService: AnalyticsService ) { super(); } async ngOnInit () { this.spinnerService.startSpinner(); await this.getIpAddresses(); this.setupHttpMethods(); this.setupAuthenticationTypes(); this.setupResponseFields(); this.setupHeaderFormArray(); this.setupFormGroup(); this.updateAuthenticationType(); this.modalReady = true; this.spinnerService.stopSpinner(); } async getIpAddresses () { const ipAddresses = await this.lookupService.getIpAddresses(); if (ipAddresses && ipAddresses.length > 0) { if (ipAddresses.length === 1) { this.alertMessage = this.i18n.translate( 'CONFIG:textForWhitelistPurposes', { ipAddresses: ipAddresses[0] }, `For whitelisting purposes our IP address is __ipAddresses__` ); } else { this.alertMessage = this.i18n.translate( 'CONFIG:textForWhitelistPurposesMutliple', { ipAddresses: ipAddresses.join(', ') }, `For whitelisting purposes our IP addresses are __ipAddresses__` ); } } } setupHttpMethods () { this.httpMethods = [ { label: this.i18n.translate( 'CONFIG:textGet', {}, 'GET' ), value: APIExternalAPI.ExternalAPIHttpMethod.Get }, { label: this.i18n.translate( 'CONFIG:textPost', {}, 'POST' ), value: APIExternalAPI.ExternalAPIHttpMethod.Post } ]; } setupAuthenticationTypes () { this.authenticationTypes = [ { label: this.i18n.translate( 'CONFIG:textBasicAuth', {}, 'Basic authentication (username and password)' ), value: APIExternalAPI.ExternalAPIAuthScheme.BasicAuth }, { label: this.i18n.translate( 'CONFIG:textHeaderAuth', {}, 'Header based authentication' ), value: APIExternalAPI.ExternalAPIAuthScheme.HeaderAuth }, { label: this.i18n.translate( 'CONFIG:textUnauthenticated', {}, 'Unauthenticated' ), value: APIExternalAPI.ExternalAPIAuthScheme.Unauthorized }, { label: this.i18n.translate( 'CONFIG:textOauth2', {}, 'OAuth 2' ), value: APIExternalAPI.ExternalAPIAuthScheme.OAuth } ]; } setHostUrl (value: string) { this.formGroup.get('url').setValue(value); } setupFormGroup () { this.formGroup = this.formBuilder.group({ name: [ this.service ? this.service.name : '', Validators.required ], description: this.service ? this.service.description : '', url: [ this.service ? this.getFormattedBody(this.service.url) : '', Validators.compose([Validators.required, this.getUrlValidator()]) ], httpMethod: [ this.service ? this.service.httpMethod : '', Validators.required ], authenticationScheme: [ this.service ? this.service.authenticationScheme : null, Validators.required ], username: this.service ? this.service.username : '', password: this.service ? this.service.password : '', authHeaderName: this.service ? this.service.authHeaderName : '', authHeaderKey: this.service ? this.service.authHeaderKey : '', oAuthClientId: this.service?.oAuthClientId ?? '', oAuthClientSecret: this.service?.oAuthClientSecret ?? '', oAuthTokenUrl: this.service?.oAuthTokenUrl ?? '', oAuthScope: this.service?.oAuthScope ?? '', postBodyTemplate: [this.service ? this.service.postBodyTemplate : '', this.getJsonValidator()], mergeModelType: APIExternalAPI.ExternalAPIMergeModelType.Applicant }, { validator: [ SiblingValueValidator( 'authHeaderName', 'authenticationScheme', APIExternalAPI.ExternalAPIAuthScheme.HeaderAuth ), SiblingValueValidator( 'authHeaderKey', 'authenticationScheme', APIExternalAPI.ExternalAPIAuthScheme.HeaderAuth ), SiblingValueValidator( 'username', 'authenticationScheme', APIExternalAPI.ExternalAPIAuthScheme.BasicAuth ), SiblingValueValidator( 'password', 'authenticationScheme', APIExternalAPI.ExternalAPIAuthScheme.BasicAuth ), SiblingValueValidator( 'oAuthClientId', 'authenticationScheme', APIExternalAPI.ExternalAPIAuthScheme.OAuth ), SiblingValueValidator( 'oAuthClientSecret', 'authenticationScheme', APIExternalAPI.ExternalAPIAuthScheme.OAuth ), SiblingValueValidator( 'oAuthTokenUrl', 'authenticationScheme', APIExternalAPI.ExternalAPIAuthScheme.OAuth ), SiblingValueValidator( 'oAuthScope', 'authenticationScheme', APIExternalAPI.ExternalAPIAuthScheme.OAuth ) ] }); this.updateHttpMethod(); } getUrlValidator (): ValidatorFn { return (control: AbstractControl) => { if (control.value) { if (this.getFormattedBody(control.value).trim().match(URL_REGEX) === null) { return { invalidURL: { i18nKey: 'common:textPleaseEnterValidURL', defaultValue: 'Please enter a valid URL' } }; } } return null; }; } getFormattedBody (value: string) { const surrogate = document.createElement('div'); surrogate.innerHTML = value; return surrogate.innerText.trim(); } getJsonValidator (): ValidatorFn { return (control: AbstractControl) => { const val = this.getFormattedBody(control.value); if ( control.root.value.httpMethod === APIExternalAPI.ExternalAPIHttpMethod.Post && val ) { try { JSON.parse(val); return null; } catch (e) { return { invalidJSON: { i18nKey: 'common:textPleaseEnterValidJSON', defaultValue: 'Please enter a valid JSON' } }; } } return null; }; } togglePasswordVisible = () => { this.passwordVisible = !this.passwordVisible; }; updateAuthenticationType () { this.authenticationType = this.formGroup.value.authenticationScheme; } updateHttpMethod () { const isPost = this.formGroup.value.httpMethod === APIExternalAPI.ExternalAPIHttpMethod.Post; // changing to post from get if (isPost && this.isPost === false) { const control = this.formGroup.get('postBodyTemplate'); control.setValue(this.getFormattedBody(control.value)); } this.isPost = isPost; } setupResponseFields () { const responseFields = this.service ? this.service.responseFields : []; this.formResponseFields = responseFields.map((field) => { return { value: field.fieldName, displayName: field.displayName }; }); this.responseFieldArray = this.formBuilder.array( this.formResponseFields.map(field => { return this.formBuilder.group({ value: [field.value, Validators.required], displayName: [field.displayName, Validators.required] }); })); } setupHeaderFormArray () { const headers = this.service?.headers ?? []; this.headers = [ ...headers ]; this.headerFormArray = this.formBuilder.array( this.headers.map(field => { return this.formBuilder.group({ headerName: [field.headerName, Validators.required], headerValue: [field.headerValue, Validators.required] }); })); } addParam () { this.editIndex = this.responseFieldArray.length; this.responseFieldArray.push( this.formBuilder.group({ displayName: ['', Validators.required], value: ['', Validators.required] })); } addHeader () { this.headerEditIndex = this.headers.length; this.headerFormArray.push( this.formBuilder.group({ headerName: ['', Validators.required], headerValue: ['', Validators.required] }) ); } editParam (paramIndex: number) { this.editIndex = paramIndex; } editHeader (headerIndex: number) { this.headerEditIndex = headerIndex; } saveParam () { if (this.formResponseFields[this.editIndex]) { // updating this.formResponseFields[this.editIndex] = this.responseFieldArray.at(this.editIndex).value; } else { // creating this.formResponseFields.push(this.responseFieldArray.at(this.editIndex).value); } this.editIndex = null; } saveHeader () { const index = this.headerEditIndex; const headerValues = this.headerFormArray.at(index).value; if (this.headers[index]) { this.headers[index] = headerValues; } else { this.headers.push(headerValues); } this.headerEditIndex = null; } cancelParam () { if (this.formResponseFields[this.editIndex]) { // canceling an edit this.responseFieldArray.at(this.editIndex).setValue(this.formResponseFields[this.editIndex]); } else { // canceling an unsaved this.responseFieldArray.removeAt(this.editIndex); } this.editIndex = null; } cancelHeader () { const index = this.headerEditIndex; const headerValues = this.headers[index]; if (headerValues) { this.headerFormArray.at(index).setValue(headerValues); } else { this.headerFormArray.removeAt(index); } this.headerEditIndex = null; } removeParam (paramIndex: number) { this.formResponseFields = [ ...this.formResponseFields.slice(0, paramIndex), ...this.formResponseFields.slice(paramIndex + 1) ]; this.responseFieldArray.removeAt(paramIndex); } removeHeader (headerIndex: number) { this.headers = [ ...this.headers.slice(0, headerIndex), ...this.headers.slice(headerIndex + 1) ]; this.headerFormArray.removeAt(headerIndex); } saveClick () { const returnValue: APIExternalAPI.ExternalAPIPayload = { ...this.formGroup.value, mergeModelType: APIExternalAPI.ExternalAPIMergeModelType.Applicant, url: this.getFormattedBody(this.formGroup.value.url), postBodyTemplate: this.getFormattedBody(this.formGroup.value.postBodyTemplate), headers: [ ...this.headers ], responseFields: this.formResponseFields.map((field) => { return { displayName: field.displayName, fieldName: field.value }; }) }; if (this.modalType !== 'create') { returnValue.id = this.service.id; } this.closeModal.emit(returnValue); this.analyticsService.emitEvent({ eventName: 'Inbound API submit', eventType: EventType.Click, extras: null }); } cancel () { this.closeModal.emit(); } }