Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webapp): [nan-839] add secondary url #2135

Merged
merged 3 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }
khaliqgant marked this conversation as resolved.
Show resolved Hide resolved
});
} 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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would make it an array, because if we have 2 why not three or more

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, but the goal was to limit this to two.

});
};

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 @@ -287,6 +287,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
Loading
Loading