mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	task: Add customHeaders as possible parameter. (#4139)
###What Adds an optional sensitive parameter for customHeaders to all current addons. It is sensitive because the user might be including api key headers.
This commit is contained in:
		
							parent
							
								
									8ff10aac29
								
							
						
					
					
						commit
						186fda1657
					
				@ -7,3 +7,13 @@ exports[`Should call datadog webhook  for archived toggle with project info 1`]
 | 
				
			|||||||
exports[`Should call datadog webhook 1`] = `"{"text":"%%% \\n some@user.com created feature toggle [some-toggle](http://some-url.com/projects//features/some-toggle) in project *undefined* \\n %%% ","title":"Unleash notification update"}"`;
 | 
					exports[`Should call datadog webhook 1`] = `"{"text":"%%% \\n some@user.com created feature toggle [some-toggle](http://some-url.com/projects//features/some-toggle) in project *undefined* \\n %%% ","title":"Unleash notification update"}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports[`Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update"}"`;
 | 
					exports[`Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update"}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports[`Should include customHeaders in headers when calling service 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update"}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports[`Should include customHeaders in headers when calling service 2`] = `
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "Content-Type": "application/json",
 | 
				
			||||||
 | 
					  "DD-API-KEY": "fakeKey",
 | 
				
			||||||
 | 
					  "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,18 @@
 | 
				
			|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
 | 
					// Jest Snapshot v1, https://goo.gl/fbAQLP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports[`Should call slack webhook 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com created feature toggle <http://some-url.com/projects/default/features/some-toggle|some-toggle> in project *default*","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
 | 
					exports[`Should call slack webhook 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com created feature toggle <http://some-url.com/projects/default/features/some-toggle|some-toggle> in project *default*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports[`Should call slack webhook for archived toggle 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle *<http://some-url.com/archive|some-toggle>*","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/archive"}]}]}"`;
 | 
					exports[`Should call slack webhook for archived toggle 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle *<http://some-url.com/archive|some-toggle>*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/archive"}]}]}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports[`Should call slack webhook for archived toggle with project info 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle *<http://some-url.com/projects/some-project/archive|some-toggle>*","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/some-project/archive"}]}]}"`;
 | 
					exports[`Should call slack webhook for archived toggle with project info 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle *<http://some-url.com/projects/some-project/archive|some-toggle>*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/some-project/archive"}]}]}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports[`Should call webhook for toggled environment 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com *disabled* <http://some-url.com/projects/default/features/some-toggle|some-toggle> in *development* environment in project *default*","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
 | 
					exports[`Should call webhook for toggled environment 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com *disabled* <http://some-url.com/projects/default/features/some-toggle|some-toggle> in *development* environment in project *default*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports[`Should include custom headers from parameters in call to service 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com *disabled* <http://some-url.com/projects/default/features/some-toggle|some-toggle> in *development* environment in project *default*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports[`Should include custom headers from parameters in call to service 2`] = `
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "Content-Type": "application/json",
 | 
				
			||||||
 | 
					  "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
				
			|||||||
@ -7,3 +7,12 @@ exports[`Should call teams webhook for archived toggle 1`] = `"{"themeColor":"00
 | 
				
			|||||||
exports[`Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":" some@user.com just archived feature toggle *[some-toggle](http://some-url.com/projects/some-project/archive)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/some-project/archive"}]}]}"`;
 | 
					exports[`Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":" some@user.com just archived feature toggle *[some-toggle](http://some-url.com/projects/some-project/archive)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/some-project/archive"}]}]}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports[`Should call teams webhook for toggled environment 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
 | 
					exports[`Should call teams webhook for toggled environment 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports[`Should include custom headers in call to teams 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exports[`Should include custom headers in call to teams 2`] = `
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "Content-Type": "application/json",
 | 
				
			||||||
 | 
					  "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
				
			|||||||
@ -40,6 +40,18 @@ const dataDogDefinition: IAddonDefinition = {
 | 
				
			|||||||
            required: true,
 | 
					            required: true,
 | 
				
			||||||
            sensitive: true,
 | 
					            sensitive: true,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'customHeaders',
 | 
				
			||||||
 | 
					            displayName: 'Extra HTTP Headers',
 | 
				
			||||||
 | 
					            placeholder: `{
 | 
				
			||||||
 | 
					                "ISTIO_USER_KEY": "hunter2",
 | 
				
			||||||
 | 
					                "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE"
 | 
				
			||||||
 | 
					            }`,
 | 
				
			||||||
 | 
					            description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`,
 | 
				
			||||||
 | 
					            required: false,
 | 
				
			||||||
 | 
					            sensitive: true,
 | 
				
			||||||
 | 
					            type: 'textfield',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    events: [
 | 
					    events: [
 | 
				
			||||||
        FEATURE_CREATED,
 | 
					        FEATURE_CREATED,
 | 
				
			||||||
 | 
				
			|||||||
@ -55,6 +55,7 @@ test('Should call datadog webhook', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parameters = {
 | 
					    const parameters = {
 | 
				
			||||||
        url: 'http://api.datadoghq.com/api/v1/events',
 | 
					        url: 'http://api.datadoghq.com/api/v1/events',
 | 
				
			||||||
 | 
					        apiKey: 'fakeKey',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await addon.handleEvent(event, parameters);
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
@ -81,6 +82,7 @@ test('Should call datadog webhook  for archived toggle', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parameters = {
 | 
					    const parameters = {
 | 
				
			||||||
        url: 'http://api.datadoghq.com/api/v1/events',
 | 
					        url: 'http://api.datadoghq.com/api/v1/events',
 | 
				
			||||||
 | 
					        apiKey: 'fakeKey',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await addon.handleEvent(event, parameters);
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
@ -108,6 +110,7 @@ test('Should call datadog webhook  for archived toggle with project info', async
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parameters = {
 | 
					    const parameters = {
 | 
				
			||||||
        url: 'http://api.datadoghq.com/api/v1/events',
 | 
					        url: 'http://api.datadoghq.com/api/v1/events',
 | 
				
			||||||
 | 
					        apiKey: 'fakeKey',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await addon.handleEvent(event, parameters);
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
@ -136,6 +139,7 @@ test(`Should call datadog webhook for toggled environment`, async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parameters = {
 | 
					    const parameters = {
 | 
				
			||||||
        url: 'http://hooks.slack.com',
 | 
					        url: 'http://hooks.slack.com',
 | 
				
			||||||
 | 
					        apiKey: 'fakeKey',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await addon.handleEvent(event, parameters);
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
@ -144,3 +148,34 @@ test(`Should call datadog webhook for toggled environment`, async () => {
 | 
				
			|||||||
    expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
 | 
					    expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
 | 
				
			||||||
    expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
 | 
					    expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test(`Should include customHeaders in headers when calling service`, async () => {
 | 
				
			||||||
 | 
					    const addon = new DatadogAddon({
 | 
				
			||||||
 | 
					        getLogger: noLogger,
 | 
				
			||||||
 | 
					        unleashUrl: 'http://some-url.com',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const event: IEvent = {
 | 
				
			||||||
 | 
					        id: 2,
 | 
				
			||||||
 | 
					        createdAt: new Date(),
 | 
				
			||||||
 | 
					        type: FEATURE_ENVIRONMENT_DISABLED,
 | 
				
			||||||
 | 
					        createdBy: 'some@user.com',
 | 
				
			||||||
 | 
					        environment: 'development',
 | 
				
			||||||
 | 
					        project: 'default',
 | 
				
			||||||
 | 
					        featureName: 'some-toggle',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					            name: 'some-toggle',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const parameters = {
 | 
				
			||||||
 | 
					        url: 'http://hooks.slack.com',
 | 
				
			||||||
 | 
					        apiKey: 'fakeKey',
 | 
				
			||||||
 | 
					        customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls).toHaveLength(1);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].url).toBe(parameters.url);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.headers).toMatchSnapshot();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,12 @@ import {
 | 
				
			|||||||
} from './feature-event-formatter-md';
 | 
					} from './feature-event-formatter-md';
 | 
				
			||||||
import { IEvent } from '../types/events';
 | 
					import { IEvent } from '../types/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IDatadogParameters {
 | 
				
			||||||
 | 
					    url: string;
 | 
				
			||||||
 | 
					    apiKey: string;
 | 
				
			||||||
 | 
					    customHeaders?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class DatadogAddon extends Addon {
 | 
					export default class DatadogAddon extends Addon {
 | 
				
			||||||
    private msgFormatter: FeatureEventFormatter;
 | 
					    private msgFormatter: FeatureEventFormatter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -21,9 +27,15 @@ export default class DatadogAddon extends Addon {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
					    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
				
			||||||
    async handleEvent(event: IEvent, parameters: any): Promise<void> {
 | 
					    async handleEvent(
 | 
				
			||||||
        const { url = 'https://api.datadoghq.com/api/v1/events', apiKey } =
 | 
					        event: IEvent,
 | 
				
			||||||
            parameters;
 | 
					        parameters: IDatadogParameters,
 | 
				
			||||||
 | 
					    ): Promise<void> {
 | 
				
			||||||
 | 
					        const {
 | 
				
			||||||
 | 
					            url = 'https://api.datadoghq.com/api/v1/events',
 | 
				
			||||||
 | 
					            apiKey,
 | 
				
			||||||
 | 
					            customHeaders,
 | 
				
			||||||
 | 
					        } = parameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const text = this.msgFormatter.format(event);
 | 
					        const text = this.msgFormatter.format(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -35,12 +47,22 @@ export default class DatadogAddon extends Addon {
 | 
				
			|||||||
            title: 'Unleash notification update',
 | 
					            title: 'Unleash notification update',
 | 
				
			||||||
            tags,
 | 
					            tags,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        let extraHeaders = {};
 | 
				
			||||||
 | 
					        if (typeof customHeaders === 'string' && customHeaders.length > 1) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                extraHeaders = JSON.parse(customHeaders);
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                this.logger.warn(
 | 
				
			||||||
 | 
					                    `Could not parse the json in the customHeaders parameter. [${customHeaders}]`,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        const requestOpts = {
 | 
					        const requestOpts = {
 | 
				
			||||||
            method: 'POST',
 | 
					            method: 'POST',
 | 
				
			||||||
            headers: {
 | 
					            headers: {
 | 
				
			||||||
                'Content-Type': 'application/json',
 | 
					                'Content-Type': 'application/json',
 | 
				
			||||||
                'DD-API-KEY': apiKey,
 | 
					                'DD-API-KEY': apiKey,
 | 
				
			||||||
 | 
					                ...extraHeaders,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            body: JSON.stringify(body),
 | 
					            body: JSON.stringify(body),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
				
			|||||||
@ -59,6 +59,18 @@ const slackDefinition: IAddonDefinition = {
 | 
				
			|||||||
            required: true,
 | 
					            required: true,
 | 
				
			||||||
            sensitive: false,
 | 
					            sensitive: false,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'customHeaders',
 | 
				
			||||||
 | 
					            displayName: 'Extra HTTP Headers',
 | 
				
			||||||
 | 
					            placeholder: `{
 | 
				
			||||||
 | 
					                "ISTIO_USER_KEY": "hunter2",
 | 
				
			||||||
 | 
					                "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE"
 | 
				
			||||||
 | 
					            }`,
 | 
				
			||||||
 | 
					            description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`,
 | 
				
			||||||
 | 
					            required: false,
 | 
				
			||||||
 | 
					            sensitive: true,
 | 
				
			||||||
 | 
					            type: 'textfield',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    events: [
 | 
					    events: [
 | 
				
			||||||
        FEATURE_CREATED,
 | 
					        FEATURE_CREATED,
 | 
				
			||||||
 | 
				
			|||||||
@ -57,6 +57,7 @@ test('Should call slack webhook', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parameters = {
 | 
					    const parameters = {
 | 
				
			||||||
        url: 'http://hooks.slack.com',
 | 
					        url: 'http://hooks.slack.com',
 | 
				
			||||||
 | 
					        defaultChannel: 'general',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await addon.handleEvent(event, parameters);
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
@ -83,6 +84,7 @@ test('Should call slack webhook for archived toggle', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parameters = {
 | 
					    const parameters = {
 | 
				
			||||||
        url: 'http://hooks.slack.com',
 | 
					        url: 'http://hooks.slack.com',
 | 
				
			||||||
 | 
					        defaultChannel: 'general',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await addon.handleEvent(event, parameters);
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
@ -110,6 +112,7 @@ test('Should call slack webhook for archived toggle with project info', async ()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parameters = {
 | 
					    const parameters = {
 | 
				
			||||||
        url: 'http://hooks.slack.com',
 | 
					        url: 'http://hooks.slack.com',
 | 
				
			||||||
 | 
					        defaultChannel: 'general',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await addon.handleEvent(event, parameters);
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
@ -138,6 +141,7 @@ test(`Should call webhook for toggled environment`, async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const parameters = {
 | 
					    const parameters = {
 | 
				
			||||||
        url: 'http://hooks.slack.com',
 | 
					        url: 'http://hooks.slack.com',
 | 
				
			||||||
 | 
					        defaultChannel: 'general',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await addon.handleEvent(event, parameters);
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
@ -255,3 +259,35 @@ test('Should post to all channels in tags', async () => {
 | 
				
			|||||||
    expect(req1.channel).toBe('#another-channel-1');
 | 
					    expect(req1.channel).toBe('#another-channel-1');
 | 
				
			||||||
    expect(req2.channel).toBe('#another-channel-2');
 | 
					    expect(req2.channel).toBe('#another-channel-2');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Should include custom headers from parameters in call to service', async () => {
 | 
				
			||||||
 | 
					    const addon = new SlackAddon({
 | 
				
			||||||
 | 
					        getLogger: noLogger,
 | 
				
			||||||
 | 
					        unleashUrl: 'http://some-url.com',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const event: IEvent = {
 | 
				
			||||||
 | 
					        id: 2,
 | 
				
			||||||
 | 
					        createdAt: new Date(),
 | 
				
			||||||
 | 
					        type: FEATURE_ENVIRONMENT_DISABLED,
 | 
				
			||||||
 | 
					        createdBy: 'some@user.com',
 | 
				
			||||||
 | 
					        environment: 'development',
 | 
				
			||||||
 | 
					        project: 'default',
 | 
				
			||||||
 | 
					        featureName: 'some-toggle',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					            name: 'some-toggle',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const parameters = {
 | 
				
			||||||
 | 
					        url: 'http://hooks.slack.com',
 | 
				
			||||||
 | 
					        defaultChannel: 'general',
 | 
				
			||||||
 | 
					        customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls).toHaveLength(1);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].url).toBe(parameters.url);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.headers).toMatchSnapshot();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,13 @@ import {
 | 
				
			|||||||
} from './feature-event-formatter-md';
 | 
					} from './feature-event-formatter-md';
 | 
				
			||||||
import { IEvent } from '../types/events';
 | 
					import { IEvent } from '../types/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ISlackAddonParameters {
 | 
				
			||||||
 | 
					    url: string;
 | 
				
			||||||
 | 
					    username?: string;
 | 
				
			||||||
 | 
					    defaultChannel: string;
 | 
				
			||||||
 | 
					    emojiIcon?: string;
 | 
				
			||||||
 | 
					    customHeaders?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
export default class SlackAddon extends Addon {
 | 
					export default class SlackAddon extends Addon {
 | 
				
			||||||
    private msgFormatter: FeatureEventFormatter;
 | 
					    private msgFormatter: FeatureEventFormatter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,12 +29,16 @@ export default class SlackAddon extends Addon {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
					    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
				
			||||||
    async handleEvent(event: IEvent, parameters: any): Promise<void> {
 | 
					    async handleEvent(
 | 
				
			||||||
 | 
					        event: IEvent,
 | 
				
			||||||
 | 
					        parameters: ISlackAddonParameters,
 | 
				
			||||||
 | 
					    ): Promise<void> {
 | 
				
			||||||
        const {
 | 
					        const {
 | 
				
			||||||
            url,
 | 
					            url,
 | 
				
			||||||
            defaultChannel,
 | 
					            defaultChannel,
 | 
				
			||||||
            username = 'Unleash',
 | 
					            username = 'Unleash',
 | 
				
			||||||
            iconEmoji = ':unleash:',
 | 
					            emojiIcon = ':unleash:',
 | 
				
			||||||
 | 
					            customHeaders,
 | 
				
			||||||
        } = parameters;
 | 
					        } = parameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const slackChannels = this.findSlackChannels(event);
 | 
					        const slackChannels = this.findSlackChannels(event);
 | 
				
			||||||
@ -42,7 +53,7 @@ export default class SlackAddon extends Addon {
 | 
				
			|||||||
        const requests = slackChannels.map((channel) => {
 | 
					        const requests = slackChannels.map((channel) => {
 | 
				
			||||||
            const body = {
 | 
					            const body = {
 | 
				
			||||||
                username,
 | 
					                username,
 | 
				
			||||||
                icon_emoji: iconEmoji, // eslint-disable-line camelcase
 | 
					                icon_emoji: emojiIcon, // eslint-disable-line camelcase
 | 
				
			||||||
                text,
 | 
					                text,
 | 
				
			||||||
                channel: `#${channel}`,
 | 
					                channel: `#${channel}`,
 | 
				
			||||||
                attachments: [
 | 
					                attachments: [
 | 
				
			||||||
@ -60,10 +71,22 @@ export default class SlackAddon extends Addon {
 | 
				
			|||||||
                    },
 | 
					                    },
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					            let extraHeaders = {};
 | 
				
			||||||
 | 
					            if (typeof customHeaders === 'string' && customHeaders.length > 1) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    extraHeaders = JSON.parse(customHeaders);
 | 
				
			||||||
 | 
					                } catch (e) {
 | 
				
			||||||
 | 
					                    this.logger.warn(
 | 
				
			||||||
 | 
					                        `Could not parse the json in the customHeaders parameter. [${customHeaders}]`,
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            const requestOpts = {
 | 
					            const requestOpts = {
 | 
				
			||||||
                method: 'POST',
 | 
					                method: 'POST',
 | 
				
			||||||
                headers: { 'Content-Type': 'application/json' },
 | 
					                headers: {
 | 
				
			||||||
 | 
					                    'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					                    ...extraHeaders,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
                body: JSON.stringify(body),
 | 
					                body: JSON.stringify(body),
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,18 @@ const teamsDefinition: IAddonDefinition = {
 | 
				
			|||||||
            required: true,
 | 
					            required: true,
 | 
				
			||||||
            sensitive: true,
 | 
					            sensitive: true,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'customHeaders',
 | 
				
			||||||
 | 
					            displayName: 'Extra HTTP Headers',
 | 
				
			||||||
 | 
					            placeholder: `{
 | 
				
			||||||
 | 
					                "ISTIO_USER_KEY": "hunter2",
 | 
				
			||||||
 | 
					                "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE"
 | 
				
			||||||
 | 
					            }`,
 | 
				
			||||||
 | 
					            description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`,
 | 
				
			||||||
 | 
					            required: false,
 | 
				
			||||||
 | 
					            sensitive: true,
 | 
				
			||||||
 | 
					            type: 'textfield',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    events: [
 | 
					    events: [
 | 
				
			||||||
        FEATURE_CREATED,
 | 
					        FEATURE_CREATED,
 | 
				
			||||||
 | 
				
			|||||||
@ -145,3 +145,34 @@ test(`Should call teams webhook for toggled environment`, async () => {
 | 
				
			|||||||
    expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
 | 
					    expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
 | 
				
			||||||
    expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
 | 
					    expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Should include custom headers in call to teams', async () => {
 | 
				
			||||||
 | 
					    const addon = new TeamsAddon({
 | 
				
			||||||
 | 
					        getLogger: noLogger,
 | 
				
			||||||
 | 
					        unleashUrl: 'http://some-url.com',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const event: IEvent = {
 | 
				
			||||||
 | 
					        id: 2,
 | 
				
			||||||
 | 
					        createdAt: new Date(),
 | 
				
			||||||
 | 
					        type: FEATURE_ENVIRONMENT_DISABLED,
 | 
				
			||||||
 | 
					        createdBy: 'some@user.com',
 | 
				
			||||||
 | 
					        environment: 'development',
 | 
				
			||||||
 | 
					        project: 'default',
 | 
				
			||||||
 | 
					        featureName: 'some-toggle',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					            name: 'some-toggle',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const parameters = {
 | 
				
			||||||
 | 
					        url: 'http://hooks.slack.com',
 | 
				
			||||||
 | 
					        customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await addon.handleEvent(event, parameters);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls).toHaveLength(1);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].url).toBe(parameters.url);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls[0].options.headers).toMatchSnapshot();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,10 @@ import {
 | 
				
			|||||||
} from './feature-event-formatter-md';
 | 
					} from './feature-event-formatter-md';
 | 
				
			||||||
import { IEvent } from '../types/events';
 | 
					import { IEvent } from '../types/events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ITeamsParameters {
 | 
				
			||||||
 | 
					    url: string;
 | 
				
			||||||
 | 
					    customHeaders?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
export default class TeamsAddon extends Addon {
 | 
					export default class TeamsAddon extends Addon {
 | 
				
			||||||
    private msgFormatter: FeatureEventFormatter;
 | 
					    private msgFormatter: FeatureEventFormatter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,8 +21,11 @@ export default class TeamsAddon extends Addon {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
					    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
				
			||||||
    async handleEvent(event: IEvent, parameters: any): Promise<void> {
 | 
					    async handleEvent(
 | 
				
			||||||
        const { url } = parameters;
 | 
					        event: IEvent,
 | 
				
			||||||
 | 
					        parameters: ITeamsParameters,
 | 
				
			||||||
 | 
					    ): Promise<void> {
 | 
				
			||||||
 | 
					        const { url, customHeaders } = parameters;
 | 
				
			||||||
        const { createdBy } = event;
 | 
					        const { createdBy } = event;
 | 
				
			||||||
        const text = this.msgFormatter.format(event);
 | 
					        const text = this.msgFormatter.format(event);
 | 
				
			||||||
        const featureLink = this.msgFormatter.featureLink(event);
 | 
					        const featureLink = this.msgFormatter.featureLink(event);
 | 
				
			||||||
@ -56,9 +63,20 @@ export default class TeamsAddon extends Addon {
 | 
				
			|||||||
            ],
 | 
					            ],
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let extraHeaders = {};
 | 
				
			||||||
 | 
					        if (typeof customHeaders === 'string' && customHeaders.length > 1) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                extraHeaders = JSON.parse(customHeaders);
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                this.logger.warn(
 | 
				
			||||||
 | 
					                    `Could not parse the json in the customHeaders parameter. [${customHeaders}]`,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const requestOpts = {
 | 
					        const requestOpts = {
 | 
				
			||||||
            method: 'POST',
 | 
					            method: 'POST',
 | 
				
			||||||
            headers: { 'Content-Type': 'application/json' },
 | 
					            headers: { 'Content-Type': 'application/json', ...extraHeaders },
 | 
				
			||||||
            body: JSON.stringify(body),
 | 
					            body: JSON.stringify(body),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        const res = await this.fetchRetry(url, requestOpts);
 | 
					        const res = await this.fetchRetry(url, requestOpts);
 | 
				
			||||||
 | 
				
			|||||||
@ -78,6 +78,18 @@ const webhookDefinition: IAddonDefinition = {
 | 
				
			|||||||
            required: false,
 | 
					            required: false,
 | 
				
			||||||
            sensitive: false,
 | 
					            sensitive: false,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            name: 'customHeaders',
 | 
				
			||||||
 | 
					            displayName: 'Extra HTTP Headers',
 | 
				
			||||||
 | 
					            placeholder: `{
 | 
				
			||||||
 | 
					                "ISTIO_USER_KEY": "hunter2",
 | 
				
			||||||
 | 
					                "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE"
 | 
				
			||||||
 | 
					            }`,
 | 
				
			||||||
 | 
					            description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`,
 | 
				
			||||||
 | 
					            required: false,
 | 
				
			||||||
 | 
					            sensitive: true,
 | 
				
			||||||
 | 
					            type: 'textfield',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    events: [
 | 
					    events: [
 | 
				
			||||||
        FEATURE_CREATED,
 | 
					        FEATURE_CREATED,
 | 
				
			||||||
 | 
				
			|||||||
@ -114,3 +114,35 @@ test('Should format event with "authorization"', () => {
 | 
				
			|||||||
    expect(call.options.headers.Authorization).toBe(parameters.authorization);
 | 
					    expect(call.options.headers.Authorization).toBe(parameters.authorization);
 | 
				
			||||||
    expect(call.options.body).toBe('feature-created on toggle some-toggle');
 | 
					    expect(call.options.body).toBe('feature-created on toggle some-toggle');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('Should handle custom headers', async () => {
 | 
				
			||||||
 | 
					    const addon = new WebhookAddon({ getLogger: noLogger });
 | 
				
			||||||
 | 
					    const event: IEvent = {
 | 
				
			||||||
 | 
					        id: 1,
 | 
				
			||||||
 | 
					        createdAt: new Date(),
 | 
				
			||||||
 | 
					        type: FEATURE_CREATED,
 | 
				
			||||||
 | 
					        createdBy: 'some@user.com',
 | 
				
			||||||
 | 
					        featureName: 'some-toggle',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					            name: 'some-toggle',
 | 
				
			||||||
 | 
					            enabled: false,
 | 
				
			||||||
 | 
					            strategies: [{ name: 'default' }],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const parameters = {
 | 
				
			||||||
 | 
					        url: 'http://test.webhook.com/plain',
 | 
				
			||||||
 | 
					        bodyTemplate: '{{event.type}} on toggle {{event.data.name}}',
 | 
				
			||||||
 | 
					        contentType: 'text/plain',
 | 
				
			||||||
 | 
					        authorization: 'API KEY 123abc',
 | 
				
			||||||
 | 
					        customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    addon.handleEvent(event, parameters);
 | 
				
			||||||
 | 
					    const call = fetchRetryCalls[0];
 | 
				
			||||||
 | 
					    expect(fetchRetryCalls.length).toBe(1);
 | 
				
			||||||
 | 
					    expect(call.url).toBe(parameters.url);
 | 
				
			||||||
 | 
					    expect(call.options.headers.Authorization).toBe(parameters.authorization);
 | 
				
			||||||
 | 
					    expect(call.options.headers.MY_CUSTOM_HEADER).toBe('MY_CUSTOM_VALUE');
 | 
				
			||||||
 | 
					    expect(call.options.body).toBe('feature-created on toggle some-toggle');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ interface IParameters {
 | 
				
			|||||||
    bodyTemplate?: string;
 | 
					    bodyTemplate?: string;
 | 
				
			||||||
    contentType?: string;
 | 
					    contentType?: string;
 | 
				
			||||||
    authorization?: string;
 | 
					    authorization?: string;
 | 
				
			||||||
 | 
					    customHeaders?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class Webhook extends Addon {
 | 
					export default class Webhook extends Addon {
 | 
				
			||||||
@ -17,7 +18,8 @@ export default class Webhook extends Addon {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async handleEvent(event: IEvent, parameters: IParameters): Promise<void> {
 | 
					    async handleEvent(event: IEvent, parameters: IParameters): Promise<void> {
 | 
				
			||||||
        const { url, bodyTemplate, contentType, authorization } = parameters;
 | 
					        const { url, bodyTemplate, contentType, authorization, customHeaders } =
 | 
				
			||||||
 | 
					            parameters;
 | 
				
			||||||
        const context = {
 | 
					        const context = {
 | 
				
			||||||
            event,
 | 
					            event,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
@ -30,11 +32,22 @@ export default class Webhook extends Addon {
 | 
				
			|||||||
            body = JSON.stringify(event);
 | 
					            body = JSON.stringify(event);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let extraHeaders = {};
 | 
				
			||||||
 | 
					        if (typeof customHeaders === 'string' && customHeaders.length > 1) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                extraHeaders = JSON.parse(customHeaders);
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                this.logger.warn(
 | 
				
			||||||
 | 
					                    `Could not parse the json in the customHeaders parameter. [${customHeaders}]`,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        const requestOpts = {
 | 
					        const requestOpts = {
 | 
				
			||||||
            method: 'POST',
 | 
					            method: 'POST',
 | 
				
			||||||
            headers: {
 | 
					            headers: {
 | 
				
			||||||
                'Content-Type': contentType || 'application/json',
 | 
					                'Content-Type': contentType || 'application/json',
 | 
				
			||||||
                Authorization: authorization || undefined,
 | 
					                Authorization: authorization || undefined,
 | 
				
			||||||
 | 
					                ...extraHeaders,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            body,
 | 
					            body,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user