Skip to content

Commit

Permalink
feat(webapp): [nan-839] add secondary url (#2135)
Browse files Browse the repository at this point in the history
## Describe your changes
Add the ability to add a secondary webhook url. I purposely treated it
as a another column to reduce complexity and to set a hard limit of the
number of webhooks we allow customers to send to. Two should be the max
IMO.

Also fixes environment name instead of the incorrect `account`

## Issue ticket number and link
NAN-839

## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [ ] I added tests, otherwise the reason is: 
- [ ] I added observability, otherwise the reason is:
- [ ] I added analytics, otherwise the reason is:

---------

Co-authored-by: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com>
  • Loading branch information
khaliqgant and bodinsamuel committed May 15, 2024
1 parent a81679a commit b007bc2
Show file tree
Hide file tree
Showing 18 changed files with 259 additions and 81 deletions.
2 changes: 1 addition & 1 deletion packages/cli/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ program
)
.option(
'--integration-id [integrationId]',
'Optional: The integration id to use for the dryrun. If not provided, the integration id will be retrieved from the nango.yaml file. This is useful using nested directories and script name are repeated'
'Optional: The integration id to use for the dryrun. If not provided, the integration id will be retrieved from the nango.yaml file. This is useful using nested directories and script names are repeated'
)
.action(async function (this: Command, sync: string, connectionId: string) {
const { autoConfirm, debug, e: environment, integrationId } = this.opts();
Expand Down
18 changes: 17 additions & 1 deletion packages/server/lib/controllers/environment.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class EnvironmentController {
const environmentVariables = await environmentService.getEnvironmentVariables(environment.id);

res.status(200).send({
account: { ...environment, env_variables: environmentVariables, host: baseUrl, uuid: account.uuid, email: user.email }
environment: { ...environment, env_variables: environmentVariables, host: baseUrl, uuid: account.uuid, email: user.email }
});
} catch (err) {
next(err);
Expand Down Expand Up @@ -171,6 +171,22 @@ class EnvironmentController {
}
}

async updateSecondaryWebhookURL(req: Request, res: Response<any, Required<RequestLocals>>, next: NextFunction) {
try {
if (!req.body) {
errorManager.errRes(res, 'missing_body');
return;
}

const { environment } = res.locals;

await environmentService.editSecondaryWebhookUrl(req.body['webhook_secondary_url'], environment.id);
res.status(200).send();
} catch (err) {
next(err);
}
}

async updateAlwaysSendWebhook(req: Request, res: Response<any, Required<RequestLocals>>, next: NextFunction) {
try {
if (!req.body) {
Expand Down
1 change: 1 addition & 0 deletions packages/server/lib/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ web.route('/api/v1/account/admin/switch').post(webAuth, accountController.switch
web.route('/api/v1/environment').get(webAuth, environmentController.getEnvironment.bind(environmentController));
web.route('/api/v1/environment/callback').post(webAuth, environmentController.updateCallback.bind(environmentController));
web.route('/api/v1/environment/webhook').post(webAuth, environmentController.updateWebhookURL.bind(environmentController));
web.route('/api/v1/environment/webhook-secondary').post(webAuth, environmentController.updateSecondaryWebhookURL.bind(environmentController));
web.route('/api/v1/environment/hmac').get(webAuth, environmentController.getHmacDigest.bind(environmentController));
web.route('/api/v1/environment/hmac-enabled').post(webAuth, environmentController.updateHmacEnabled.bind(environmentController));
web.route('/api/v1/environment/webhook-send').post(webAuth, environmentController.updateAlwaysSendWebhook.bind(environmentController));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exports.up = async function (knex, _) {
return knex.schema.alterTable('_nango_environments', function (table) {
table.text('webhook_url_secondary');
});
};

exports.down = function (knex, _) {
return knex.schema.alterTable('_nango_environments', function (table) {
table.dropColumn('webhook_url_secondary');
});
};
1 change: 1 addition & 0 deletions packages/shared/lib/models/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface Environment extends Timestamps {
secret_key_hashed?: string | null;
callback_url: string | null;
webhook_url: string | null;
webhook_url_secondary: string | null;
websockets_path?: string | null;
hmac_enabled: boolean;
always_send_webhook: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ describe('Environment service', () => {
slack_notifications: false,
updated_at: expect.toBeIsoDate(),
uuid: expect.any(String),
webhook_url: null
webhook_url: null,
webhook_url_secondary: null
});

expect(env.secret_key).not.toEqual(env.secret_key_hashed);
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/lib/services/environment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ class EnvironmentService {
return db.knex.from<Environment>(TABLE).where({ id }).update({ webhook_url: webhookUrl }, ['id']);
}

async editSecondaryWebhookUrl(webhookUrl: string, id: number): Promise<Environment | null> {
return db.knex.from<Environment>(TABLE).where({ id }).update({ webhook_url_secondary: webhookUrl }, ['id']);
}

async editHmacEnabled(hmacEnabled: boolean, id: number): Promise<Environment | null> {
return db.knex.from<Environment>(TABLE).where({ id }).update({ hmac_enabled: hmacEnabled }, ['id']);
}
Expand Down
85 changes: 49 additions & 36 deletions packages/shared/lib/services/notification/webhook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ class WebhookService {
): Promise<{ send: boolean; environmentInfo: Environment | null }> {
const environmentInfo = await environmentService.getById(environment_id);
const hasWebhookUrl = environmentInfo?.webhook_url;
const hasSecondaryWebhookUrl = environmentInfo?.webhook_url_secondary;

if (options?.forward && hasWebhookUrl) {
if ((options?.forward && hasWebhookUrl) || hasSecondaryWebhookUrl) {
return { send: true, environmentInfo };
}

Expand All @@ -128,11 +129,11 @@ class WebhookService {
) {
const { environmentInfo } = await this.shouldSendWebhook(nangoConnection.environment_id);

if (!environmentInfo || !environmentInfo.webhook_url) {
if (!environmentInfo || (!environmentInfo.webhook_url && !environmentInfo.webhook_url_secondary)) {
return;
}

const { webhook_url: webhookUrl, always_send_webhook: alwaysSendWebhook } = environmentInfo;
const { webhook_url: webhookUrl, webhook_url_secondary: webhookUrlSecondary, always_send_webhook: alwaysSendWebhook } = environmentInfo;

const noChanges =
responseResults.added === 0 && responseResults.updated === 0 && (responseResults.deleted === 0 || responseResults.deleted === undefined);
Expand Down Expand Up @@ -179,48 +180,60 @@ class WebhookService {
? 'with no data changes as per your environment settings.'
: `with the following data: ${JSON.stringify(body, null, 2)}`;

try {
const headers = this.getSignatureHeader(environmentInfo.secret_key, body);
const webhookUrls: { url: string; type: string }[] = [
{ url: webhookUrl, type: 'webhookUrl' },
{ url: webhookUrlSecondary, type: 'webhookUrlSecondary' }
].filter((webhook) => webhook.url) as { url: string; type: string }[];

const response = await backOff(
() => {
return axios.post(webhookUrl, body, { headers });
},
{ numOfAttempts: RETRY_ATTEMPTS, retry: this.retry.bind(this, activityLogId, environment_id, logCtx) }
);
for (const webhookUrl of webhookUrls) {
const { url, type } = webhookUrl;
try {
const headers = this.getSignatureHeader(environmentInfo.secret_key, body);

const response = await backOff(
() => {
return axios.post(url, body, { headers });
},
{ numOfAttempts: RETRY_ATTEMPTS, retry: this.retry.bind(this, activityLogId, environment_id, logCtx) }
);

if (response.status >= 200 && response.status < 300) {
await createActivityLogMessage({
level: 'info',
environment_id,
activity_log_id: activityLogId,
content: `Sync webhook sent successfully ${type === 'webhookUrlSecondary' ? 'to the secondary webhook URL' : ''} and received with a ${response.status} response code to ${url} ${endingMessage}`,
timestamp: Date.now()
});
await logCtx.info(
`Sync webhook sent successfully ${type === 'webhookUrlSecondary' ? 'to the secondary webhook URL' : ''} and received with a ${response.status} response code to ${url} ${endingMessage}`
);
} else {
await createActivityLogMessage({
level: 'error',
environment_id,
activity_log_id: activityLogId,
content: `Sync webhook sent successfully ${type === 'webhookUrlSecondary' ? 'to the secondary webhook URL' : ''} to ${url} ${endingMessage} but received a ${response.status} response code. Please send a 2xx on successful receipt.`,
timestamp: Date.now()
});
await logCtx.error(
`Sync webhook sent successfully ${type === 'webhookUrlSecondary' ? 'to the secondary webhook URL' : ''} to ${url} ${endingMessage} but received a ${response.status} response code. Please send a 2xx on successful receipt.`
);
}
} catch (e) {
const errorMessage = stringifyError(e, { pretty: true });

if (response.status >= 200 && response.status < 300) {
await createActivityLogMessage({
level: 'info',
environment_id,
activity_log_id: activityLogId,
content: `Sync webhook sent successfully and received with a ${response.status} response code to ${webhookUrl} ${endingMessage}`,
timestamp: Date.now()
});
await logCtx.info(`Sync webhook sent successfully and received with a ${response.status} response code to ${webhookUrl} ${endingMessage}`);
} else {
await createActivityLogMessage({
level: 'error',
environment_id,
activity_log_id: activityLogId,
content: `Sync webhook sent successfully to ${webhookUrl} ${endingMessage} but received a ${response.status} response code. Please send a 2xx on successful receipt.`,
content: `Sync webhook failed to send ${type === 'webhookUrlSecondary' ? 'to the secondary webhook URL' : ''} to ${url}. The error was: ${errorMessage}`,
timestamp: Date.now()
});
await logCtx.error(
`Sync webhook sent successfully to ${webhookUrl} ${endingMessage} but received a ${response.status} response code. Please send a 2xx on successful receipt.`
);
await logCtx.error(`Sync webhook failed to send ${type === 'webhookUrlSecondary' ? 'to the secondary webhook URL' : ''} to ${url}`, {
error: e
});
}
} catch (e) {
const errorMessage = stringifyError(e, { pretty: true });

await createActivityLogMessage({
level: 'error',
environment_id,
activity_log_id: activityLogId,
content: `Sync webhook failed to send to ${webhookUrl}. The error was: ${errorMessage}`,
timestamp: Date.now()
});
await logCtx.error(`Sync webhook failed to send to ${webhookUrl}`, { error: e });
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/webapp/src/hooks/useEnvironment.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import useSWR from 'swr';
import type { Account } from '../types';
import type { Environment } from '../types';
import { swrFetcher } from '../utils/api';

export function useEnvironment(env: string) {
const { data, error, mutate } = useSWR<{ account: Account }>(`/api/v1/environment?env=${env}`, swrFetcher, {});
const { data, error, mutate } = useSWR<{ environment: Environment }>(`/api/v1/environment?env=${env}`, swrFetcher, {});

const loading = !data && !error;

return {
loading,
error,
environment: data?.account,
environment: data?.environment,
mutate
};
}
6 changes: 5 additions & 1 deletion packages/webapp/src/pages/Connection/Syncs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,11 @@ export default function Syncs({ syncs, connection, reload, loaded, syncLoaded, e
className={`flex items-center px-2 py-3 text-[13px] ${syncCommandButtonsDisabled ? '' : 'cursor-pointer'} justify-between border-b border-border-gray`}
>
<div className="flex items-center w-52">
<div className="w-36 max-w-3xl ml-1 truncate">{Array.isArray(sync.models) ? sync.models.join(', ') : sync.models}</div>
<Tooltip text={sync.name} type="dark">
<div className="w-36 max-w-3xl ml-1 truncate">
{Array.isArray(sync.models) ? sync.models.join(', ') : sync.models}
</div>
</Tooltip>
</div>
<div className="flex w-20 -ml-2">
<span className="">
Expand Down

0 comments on commit b007bc2

Please sign in to comment.