📧 Dynamic webhook management (#646)

* Make channels section expandable based on override setting

* Revert "Make channels section expandable based on override setting"

This reverts commit e107c5e990b4110156043ed494fb0054bd936654.

* Add status  component

* Remove no longer used component

* Remove no longer used component

* Removed webhook creation during App installation

* Extend tRPC meta to contain webhook sync flag

* Add app baseUrl to the context

* Webhook management service

* Add changeset
This commit is contained in:
Krzysztof Wolski 2023-06-20 11:38:32 +02:00 committed by GitHub
parent ec68ed53a3
commit 82dfc3fa6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1042 additions and 159 deletions

View file

@ -0,0 +1,7 @@
---
"saleor-app-emails-and-messages": minor
---
Webhooks are no longer created during the app registration. Instead, the app will subscribe events based on it's configuration, after change has been detected.
This change does not have negative impact on existing app installations - webhooks will be removed during next change of the provider configuration.

View file

@ -1,39 +0,0 @@
import { Client, gql } from "urql";
import { WebhookCreateMutationDocument, WebhookEventTypeEnum } from "../../generated/graphql";
import { notifyWebhook } from "../pages/api/webhooks/notify";
const webhookCreateMutation = gql`
mutation webhookCreateMutation($input: WebhookCreateInput!) {
webhookCreate(input: $input) {
webhook {
id
name
isActive
}
errors {
field
message
}
}
}
`;
interface RegisterNotifyWebhookArgs {
client: Client;
baseUrl: string;
}
export const registerNotifyWebhook = async ({ client, baseUrl }: RegisterNotifyWebhookArgs) => {
const manifest = notifyWebhook.getWebhookManifest(baseUrl);
return await client
.mutation(WebhookCreateMutationDocument, {
input: {
name: manifest.name,
targetUrl: manifest.targetUrl,
events: [WebhookEventTypeEnum.NotifyUser],
isActive: true,
},
})
.toPromise();
};

View file

@ -10,18 +10,13 @@ import {
sendgridUpdateEventSchema,
sendgridUpdateSenderSchema,
} from "./sendgrid-config-input-schema";
import {
SendgridConfigurationService,
SendgridConfigurationServiceError,
} from "./sendgrid-configuration.service";
import { SendgridConfigurationServiceError } from "./sendgrid-configuration.service";
import { router } from "../../trpc/trpc-server";
import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
import { TRPCError } from "@trpc/server";
import { fetchSenders } from "../sendgrid-api";
import { updateChannelsInputSchema } from "../../channels/channel-configuration-schema";
import { SendgridPrivateMetadataManager } from "./sendgrid-metadata-manager";
import { createSettingsManager } from "../../../lib/metadata-manager";
import { sendgridDefaultEmptyConfigurations } from "./sendgrid-default-empty-configurations";
import { protectedWithConfigurationServices } from "../../trpc/protected-client-procedure-with-services";
export const throwTrpcErrorFromConfigurationServiceError = (
error: SendgridConfigurationServiceError | unknown
@ -51,32 +46,14 @@ export const throwTrpcErrorFromConfigurationServiceError = (
});
};
/*
* Allow access only for the dashboard users and attaches the
* configuration service to the context
*/
const protectedWithConfigurationService = protectedClientProcedure.use(({ next, ctx }) =>
next({
ctx: {
...ctx,
sendgridConfigurationService: new SendgridConfigurationService({
metadataManager: new SendgridPrivateMetadataManager(
createSettingsManager(ctx.apiClient, ctx.appId!),
ctx.saleorApiUrl
),
}),
},
})
);
export const sendgridConfigurationRouter = router({
fetch: protectedWithConfigurationService.query(async ({ ctx }) => {
fetch: protectedWithConfigurationServices.query(async ({ ctx }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug("sendgridConfigurationRouter.fetch called");
return ctx.sendgridConfigurationService.getConfigurationRoot();
}),
getConfiguration: protectedWithConfigurationService
getConfiguration: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(sendgridConfigurationIdInputSchema)
.query(async ({ ctx, input }) => {
@ -89,7 +66,7 @@ export const sendgridConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
getConfigurations: protectedWithConfigurationService
getConfigurations: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(sendgridGetConfigurationsInputSchema)
.query(async ({ ctx, input }) => {
@ -102,8 +79,8 @@ export const sendgridConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
createConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
createConfiguration: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(sendgridCreateConfigurationInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -116,8 +93,8 @@ export const sendgridConfigurationRouter = router({
return await ctx.sendgridConfigurationService.createConfiguration(newConfiguration);
}),
deleteConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
deleteConfiguration: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(sendgridConfigurationIdInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -130,7 +107,7 @@ export const sendgridConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
getEventConfiguration: protectedWithConfigurationService
getEventConfiguration: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(sendgridGetEventConfigurationInputSchema)
.query(async ({ ctx, input }) => {
@ -147,8 +124,8 @@ export const sendgridConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
updateBasicInformation: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
updateBasicInformation: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(sendgridUpdateBasicInformationSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -161,7 +138,7 @@ export const sendgridConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
updateApiConnection: protectedWithConfigurationService
updateApiConnection: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(sendgridUpdateApiConnectionSchema)
.mutation(async ({ ctx, input }) => {
@ -176,7 +153,7 @@ export const sendgridConfigurationRouter = router({
}
}),
updateSender: protectedWithConfigurationService
updateSender: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(sendgridUpdateSenderSchema)
.mutation(async ({ ctx, input }) => {
@ -213,7 +190,7 @@ export const sendgridConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
updateChannels: protectedWithConfigurationService
updateChannels: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(updateChannelsInputSchema)
.mutation(async ({ ctx, input }) => {
@ -235,8 +212,8 @@ export const sendgridConfigurationRouter = router({
}
}),
updateEvent: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
updateEvent: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(sendgridUpdateEventSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -256,8 +233,8 @@ export const sendgridConfigurationRouter = router({
}
}),
updateEventArray: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
updateEventArray: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(sendgridUpdateEventArraySchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });

View file

@ -1,10 +1,6 @@
import { createLogger } from "@saleor/apps-shared";
import {
SmtpConfigurationService,
SmtpConfigurationServiceError,
} from "./smtp-configuration.service";
import { SmtpConfigurationServiceError } from "./smtp-configuration.service";
import { router } from "../../trpc/trpc-server";
import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
import { z } from "zod";
import { compileMjml } from "../compile-mjml";
import Handlebars from "handlebars";
@ -22,8 +18,7 @@ import {
smtpUpdateSmtpSchema,
} from "./smtp-config-input-schema";
import { updateChannelsInputSchema } from "../../channels/channel-configuration-schema";
import { SmtpPrivateMetadataManager } from "./smtp-metadata-manager";
import { createSettingsManager } from "../../../lib/metadata-manager";
import { protectedWithConfigurationServices } from "../../trpc/protected-client-procedure-with-services";
import { smtpDefaultEmptyConfigurations } from "./smtp-default-empty-configurations";
export const throwTrpcErrorFromConfigurationServiceError = (
@ -54,32 +49,14 @@ export const throwTrpcErrorFromConfigurationServiceError = (
});
};
/*
* Allow access only for the dashboard users and attaches the
* configuration service to the context
*/
const protectedWithConfigurationService = protectedClientProcedure.use(({ next, ctx }) =>
next({
ctx: {
...ctx,
smtpConfigurationService: new SmtpConfigurationService({
metadataManager: new SmtpPrivateMetadataManager(
createSettingsManager(ctx.apiClient, ctx.appId!),
ctx.saleorApiUrl
),
}),
},
})
);
export const smtpConfigurationRouter = router({
fetch: protectedWithConfigurationService.query(async ({ ctx }) => {
fetch: protectedWithConfigurationServices.query(async ({ ctx }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug("smtpConfigurationRouter.fetch called");
return ctx.smtpConfigurationService.getConfigurationRoot();
}),
getConfiguration: protectedWithConfigurationService
getConfiguration: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(smtpConfigurationIdInputSchema)
.query(async ({ ctx, input }) => {
@ -93,7 +70,7 @@ export const smtpConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
getConfigurations: protectedWithConfigurationService
getConfigurations: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(smtpGetConfigurationsInputSchema)
.query(async ({ ctx, input }) => {
@ -106,8 +83,8 @@ export const smtpConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
createConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
createConfiguration: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(smtpCreateConfigurationInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -120,8 +97,8 @@ export const smtpConfigurationRouter = router({
return await ctx.smtpConfigurationService.createConfiguration(newConfiguration);
}),
deleteConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
deleteConfiguration: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(smtpConfigurationIdInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -134,7 +111,7 @@ export const smtpConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
getEventConfiguration: protectedWithConfigurationService
getEventConfiguration: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(smtpGetEventConfigurationInputSchema)
.query(async ({ ctx, input }) => {
@ -152,7 +129,7 @@ export const smtpConfigurationRouter = router({
}
}),
renderTemplate: protectedWithConfigurationService
renderTemplate: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(
z.object({
@ -196,8 +173,8 @@ export const smtpConfigurationRouter = router({
};
}),
updateBasicInformation: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
updateBasicInformation: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(smtpUpdateBasicInformationSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -211,8 +188,8 @@ export const smtpConfigurationRouter = router({
}
}),
updateSmtp: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
updateSmtp: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(smtpUpdateSmtpSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -226,7 +203,7 @@ export const smtpConfigurationRouter = router({
}
}),
updateSender: protectedWithConfigurationService
updateSender: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(smtpUpdateSenderSchema)
.mutation(async ({ ctx, input }) => {
@ -241,7 +218,7 @@ export const smtpConfigurationRouter = router({
}
}),
updateChannels: protectedWithConfigurationService
updateChannels: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(updateChannelsInputSchema)
.mutation(async ({ ctx, input }) => {
@ -263,8 +240,8 @@ export const smtpConfigurationRouter = router({
}
}),
updateEvent: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
updateEvent: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(smtpUpdateEventSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -283,8 +260,8 @@ export const smtpConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
updateEventActiveStatus: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
updateEventActiveStatus: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(smtpUpdateEventActiveStatusInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
@ -302,8 +279,8 @@ export const smtpConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
updateEventArray: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
updateEventArray: protectedWithConfigurationServices
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
.input(smtpUpdateEventArraySchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });

View file

@ -0,0 +1,57 @@
import { createLogger } from "@saleor/apps-shared";
import { createSettingsManager } from "../../lib/metadata-manager";
import { SendgridConfigurationService } from "../sendgrid/configuration/sendgrid-configuration.service";
import { SendgridPrivateMetadataManager } from "../sendgrid/configuration/sendgrid-metadata-manager";
import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service";
import { SmtpPrivateMetadataManager } from "../smtp/configuration/smtp-metadata-manager";
import { syncWebhookStatus } from "../webhook-management/sync-webhook-status";
import { protectedClientProcedure } from "./protected-client-procedure";
import { WebhookManagementService } from "../webhook-management/webhook-management-service";
const logger = createLogger({ name: "protectedWithConfigurationServices middleware" });
/*
* Allow access only for the dashboard users and attaches the
* configuration service to the context.
* The services do not fetch data from the API unless they are used.
* If meta key updateWebhooks is set to true, additional calls to the API will be made
* to create or remove webhooks.
*/
export const protectedWithConfigurationServices = protectedClientProcedure.use(
async ({ next, ctx, meta }) => {
const smtpConfigurationService = new SmtpConfigurationService({
metadataManager: new SmtpPrivateMetadataManager(
createSettingsManager(ctx.apiClient, ctx.appId!),
ctx.saleorApiUrl
),
});
const sendgridConfigurationService = new SendgridConfigurationService({
metadataManager: new SendgridPrivateMetadataManager(
createSettingsManager(ctx.apiClient, ctx.appId!),
ctx.saleorApiUrl
),
});
const result = await next({
ctx: {
smtpConfigurationService,
sendgridConfigurationService,
},
});
if (meta?.updateWebhooks) {
logger.debug("Updating webhooks");
const webhookManagementService = new WebhookManagementService(ctx.baseUrl, ctx.apiClient);
await syncWebhookStatus({
sendgridConfigurationService,
smtpConfigurationService,
webhookManagementService,
});
}
return result;
}
);

View file

@ -1,13 +1,17 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
import { inferAsyncReturnType } from "@trpc/server";
import { getBaseUrl } from "../../lib/get-base-url";
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
const baseUrl = getBaseUrl(req.headers);
return {
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,
saleorApiUrl: req.headers[SALEOR_API_URL_HEADER] as string | undefined,
appId: undefined as undefined | string,
ssr: undefined as undefined | boolean,
baseUrl,
};
};

View file

@ -5,6 +5,7 @@ import { ZodError } from "zod";
interface Meta {
requiredClientPermissions?: Permission[];
updateWebhooks?: boolean;
}
const t = initTRPC

View file

@ -0,0 +1,105 @@
import { Client, gql } from "urql";
import {
AppWebhooksDocument,
CreateAppWebhookDocument,
CreateAppWebhookMutationVariables,
DeleteAppWebhookDocument,
} from "../../../generated/graphql";
gql`
fragment WebhookDetails on Webhook {
id
name
asyncEvents {
name
eventType
}
isActive
}
query AppWebhooks {
app {
webhooks {
...WebhookDetails
}
}
}
mutation CreateAppWebhook($input: WebhookCreateInput!) {
webhookCreate(input: $input) {
errors {
field
message
}
webhook {
...WebhookDetails
}
}
}
mutation DeleteAppWebhook($id: ID!) {
webhookDelete(id: $id) {
errors {
field
message
}
webhook {
...WebhookDetails
}
}
}
`;
export const fetchAppWebhooks = ({ client }: { client: Client }) =>
client
.query(AppWebhooksDocument, {})
.toPromise()
.then((response) => {
if (response.error) {
throw new Error(response.error.message);
}
const appData = response.data?.app;
if (!appData) {
throw new Error(
"App data not found in the response. The token can be invalid or the app has been uninstalled."
);
}
return appData.webhooks || [];
});
export const createAppWebhook = ({
client,
variables,
}: {
client: Client;
variables: CreateAppWebhookMutationVariables["input"];
}) =>
client
.mutation(CreateAppWebhookDocument, { input: variables })
.toPromise()
.then((response) => {
if (response.error) {
throw new Error(response.error.message);
}
console.log("create wh", response.data?.webhookCreate?.errors);
const webhookCreateData = response.data?.webhookCreate?.webhook;
if (!webhookCreateData) {
throw new Error("Webhook Creation did not return any data nor error.");
}
return webhookCreateData;
});
export const deleteAppWebhook = ({ client, id }: { client: Client; id: string }) =>
client
.mutation(DeleteAppWebhookDocument, { id })
.toPromise()
.then((response) => {
if (response.error) {
throw new Error(response.error.message);
}
return;
});

View file

@ -0,0 +1,287 @@
import { expect, describe, it } from "vitest";
import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema";
import { getWebhookStatusesFromConfigurations } from "./get-webhook-statuses-from-configurations";
import { SendgridConfiguration } from "../sendgrid/configuration/sendgrid-config-schema";
export const nonActiveSmtpConfiguration: SmtpConfiguration = {
id: "1685343953413npk9p",
active: false,
name: "Best name",
smtpHost: "smtpHost",
smtpPort: "1337",
encryption: "NONE",
channels: {
override: false,
channels: [],
mode: "restrict",
},
events: [
{
active: false,
eventType: "ORDER_CREATED",
template: "template",
subject: "Order {{ order.number }} has been created!!",
},
{
active: false,
eventType: "ORDER_FULFILLED",
template: "template",
subject: "Order {{ order.number }} has been fulfilled",
},
{
active: false,
eventType: "ORDER_CONFIRMED",
template: "template",
subject: "Order {{ order.number }} has been confirmed",
},
{
active: false,
eventType: "ORDER_CANCELLED",
template: "template",
subject: "Order {{ order.number }} has been cancelled",
},
{
active: false,
eventType: "ORDER_FULLY_PAID",
template: "template",
subject: "Order {{ order.number }} has been fully paid",
},
{
active: false,
eventType: "INVOICE_SENT",
template: "template",
subject: "New invoice has been created",
},
{
active: false,
eventType: "ACCOUNT_CONFIRMATION",
template: "template",
subject: "Account activation",
},
{
active: false,
eventType: "ACCOUNT_PASSWORD_RESET",
template: "template",
subject: "Password reset request",
},
{
active: false,
eventType: "ACCOUNT_CHANGE_EMAIL_REQUEST",
template: "template",
subject: "Email change request",
},
{
active: false,
eventType: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
template: "template",
subject: "Email change confirmation",
},
{
active: false,
eventType: "ACCOUNT_DELETE",
template: "template",
subject: "Account deletion",
},
],
smtpUser: "John",
smtpPassword: "securepassword",
senderEmail: "no-reply@example.com",
senderName: "Sender Name",
};
const nonActiveSendgridConfiguration: SendgridConfiguration = {
id: "1685343953413npk9p",
active: false,
name: "Best name",
sandboxMode: false,
apiKey: "SG.123",
channels: {
override: false,
channels: [],
mode: "restrict",
},
events: [
{
active: false,
eventType: "ORDER_CREATED",
template: "1",
},
{
active: false,
eventType: "ORDER_FULFILLED",
template: undefined,
},
{
active: false,
eventType: "ORDER_CONFIRMED",
template: undefined,
},
{
active: false,
eventType: "ORDER_CANCELLED",
template: undefined,
},
{
active: false,
eventType: "ORDER_FULLY_PAID",
template: undefined,
},
{
active: false,
eventType: "INVOICE_SENT",
template: undefined,
},
{
active: false,
eventType: "ACCOUNT_CONFIRMATION",
template: undefined,
},
{
active: false,
eventType: "ACCOUNT_PASSWORD_RESET",
template: undefined,
},
{
active: false,
eventType: "ACCOUNT_CHANGE_EMAIL_REQUEST",
template: undefined,
},
{
active: false,
eventType: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
template: undefined,
},
{
active: false,
eventType: "ACCOUNT_DELETE",
template: undefined,
},
],
sender: "1",
senderEmail: "no-reply@example.com",
senderName: "Sender Name",
};
describe("getWebhookStatusesFromConfigurations", function () {
it("Statuses should be set to false, when no configurations passed", async () => {
expect(
getWebhookStatusesFromConfigurations({
smtpConfigurations: [],
sendgridConfigurations: [],
})
).toStrictEqual({
invoiceSentWebhook: false,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
});
it("Statuses should be set to false, when no active configurations passed", async () => {
expect(
getWebhookStatusesFromConfigurations({
smtpConfigurations: [nonActiveSmtpConfiguration],
sendgridConfigurations: [nonActiveSendgridConfiguration],
})
).toStrictEqual({
invoiceSentWebhook: false,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
});
it("Statuses should be set to false, when configuration is not active even if events were activated", async () => {
const smtpConfiguration = {
...nonActiveSmtpConfiguration,
events: nonActiveSmtpConfiguration.events.map((event) => ({ ...event, active: true })),
};
expect(
getWebhookStatusesFromConfigurations({
smtpConfigurations: [smtpConfiguration],
sendgridConfigurations: [nonActiveSendgridConfiguration],
})
).toStrictEqual({
invoiceSentWebhook: false,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
});
it("Status of the event should be set to true, when at least one active configuration has activated it", async () => {
const smtpConfiguration: SmtpConfiguration = {
...nonActiveSmtpConfiguration,
active: true,
events: [
{
active: true,
eventType: "INVOICE_SENT",
subject: "",
template: "",
},
],
};
expect(
getWebhookStatusesFromConfigurations({
smtpConfigurations: [nonActiveSmtpConfiguration, smtpConfiguration],
sendgridConfigurations: [nonActiveSendgridConfiguration],
})
).toStrictEqual({
invoiceSentWebhook: true,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
});
it("Status of the NOTIFY webhooks should be set to true, when at least one active configuration has activated one of its related events", async () => {
const smtpConfiguration: SmtpConfiguration = {
...nonActiveSmtpConfiguration,
active: true,
events: [
{
active: false,
eventType: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
subject: "",
template: "",
},
{
active: true,
eventType: "ACCOUNT_CHANGE_EMAIL_REQUEST",
subject: "",
template: "",
},
],
};
expect(
getWebhookStatusesFromConfigurations({
smtpConfigurations: [nonActiveSmtpConfiguration, smtpConfiguration],
sendgridConfigurations: [nonActiveSendgridConfiguration],
})
).toStrictEqual({
invoiceSentWebhook: false,
notifyWebhook: true,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
});
});

View file

@ -0,0 +1,54 @@
import { SendgridConfiguration } from "../sendgrid/configuration/sendgrid-config-schema";
import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema";
import { AppWebhook, eventToWebhookMapping } from "./webhook-management-service";
/*
* Returns dictionary of webhook statuses based on passed configurations.
* Webhook is marked as active (true) if at least one event related event is marked as active.
*/
export const getWebhookStatusesFromConfigurations = ({
smtpConfigurations,
sendgridConfigurations,
}: {
smtpConfigurations: SmtpConfiguration[];
sendgridConfigurations: SendgridConfiguration[];
}) => {
const statuses: Record<AppWebhook, boolean> = {
invoiceSentWebhook: false,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
};
smtpConfigurations.forEach(async (config) => {
if (!config.active) {
// Configuration has to be active to enable webhook
return;
}
config.events.forEach(async (event) => {
if (event.active) {
/*
* Mapping is mandatory since multiple events can be mapped to one webhook,
* as in case of NOTIFY
*/
statuses[eventToWebhookMapping[event.eventType]] = true;
}
});
});
sendgridConfigurations.forEach(async (config) => {
if (!config.active) {
return;
}
config.events.forEach(async (event) => {
if (event.active) {
statuses[eventToWebhookMapping[event.eventType]] = true;
}
});
});
return statuses;
};

View file

@ -0,0 +1,159 @@
import { vi, expect, describe, it, afterEach } from "vitest";
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { SmtpPrivateMetadataManager } from "../smtp/configuration/smtp-metadata-manager";
import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service";
import { syncWebhookStatus } from "./sync-webhook-status";
import { SendgridPrivateMetadataManager } from "../sendgrid/configuration/sendgrid-metadata-manager";
import { SendgridConfigurationService } from "../sendgrid/configuration/sendgrid-configuration.service";
import { WebhookManagementService } from "./webhook-management-service";
import { Client } from "urql";
import * as statusesExports from "./get-webhook-statuses-from-configurations";
const mockSaleorApiUrl = "https://demo.saleor.io/graphql/";
describe("syncWebhookStatus", function () {
const createMockedClient = () => ({} as Client);
const webhookManagementService = new WebhookManagementService(
mockSaleorApiUrl,
createMockedClient()
);
const createWebhookMock = vi
.spyOn(webhookManagementService, "createWebhook")
.mockImplementation((_) => Promise.resolve());
const deleteWebhookMock = vi
.spyOn(webhookManagementService, "deleteWebhook")
.mockImplementation((_) => Promise.resolve());
const smtpConfigurator = new SmtpPrivateMetadataManager(
null as unknown as SettingsManager,
mockSaleorApiUrl
);
const smtpConfigurationService = new SmtpConfigurationService({
metadataManager: smtpConfigurator,
initialData: {
configurations: [],
},
});
const sendgridConfigurator = new SendgridPrivateMetadataManager(
null as unknown as SettingsManager,
mockSaleorApiUrl
);
const sendgridConfigurationService = new SendgridConfigurationService({
metadataManager: sendgridConfigurator,
initialData: {
configurations: [],
},
});
afterEach(() => {
vi.resetAllMocks();
});
it("No webhook should be created or deleted, when both API and configurations don't use any", async () => {
vi.spyOn(statusesExports, "getWebhookStatusesFromConfigurations").mockReturnValue({
invoiceSentWebhook: false,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
const getWebhooksStatusMock = vi
.spyOn(webhookManagementService, "getWebhooksStatus")
.mockResolvedValue({
invoiceSentWebhook: false,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
await syncWebhookStatus({
smtpConfigurationService,
sendgridConfigurationService,
webhookManagementService,
});
expect(getWebhooksStatusMock).toHaveBeenCalled();
expect(deleteWebhookMock).not.toHaveBeenCalled();
expect(createWebhookMock).not.toHaveBeenCalled();
});
it("Webhooks should be deleted from API, when configurations no longer use them", async () => {
vi.spyOn(statusesExports, "getWebhookStatusesFromConfigurations").mockReturnValue({
invoiceSentWebhook: false,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
const getWebhooksStatusMock = vi
.spyOn(webhookManagementService, "getWebhooksStatus")
.mockResolvedValue({
invoiceSentWebhook: true,
notifyWebhook: true,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
await syncWebhookStatus({
smtpConfigurationService,
sendgridConfigurationService,
webhookManagementService,
});
expect(getWebhooksStatusMock).toHaveBeenCalled();
expect(createWebhookMock).not.toHaveBeenCalled();
expect(deleteWebhookMock).toHaveBeenCalledTimes(2);
});
it("Webhooks should be created using API, when new configurations use them", async () => {
vi.spyOn(statusesExports, "getWebhookStatusesFromConfigurations").mockReturnValue({
invoiceSentWebhook: true,
notifyWebhook: true,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
const getWebhooksStatusMock = vi
.spyOn(webhookManagementService, "getWebhooksStatus")
.mockResolvedValue({
invoiceSentWebhook: false,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderFulfilledWebhook: false,
orderCreatedWebhook: false,
orderFullyPaidWebhook: false,
});
await syncWebhookStatus({
smtpConfigurationService,
sendgridConfigurationService,
webhookManagementService,
});
expect(getWebhooksStatusMock).toHaveBeenCalled();
expect(createWebhookMock).toHaveBeenCalledTimes(2);
expect(deleteWebhookMock).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,60 @@
import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service";
import { SendgridConfigurationService } from "../sendgrid/configuration/sendgrid-configuration.service";
import { WebhookManagementService, AppWebhook } from "./webhook-management-service";
import { createLogger } from "@saleor/apps-shared";
import { getWebhookStatusesFromConfigurations } from "./get-webhook-statuses-from-configurations";
const logger = createLogger({ name: "SyncWebhooksStatus" });
interface SyncWebhooksStatusArgs {
smtpConfigurationService: SmtpConfigurationService;
sendgridConfigurationService: SendgridConfigurationService;
webhookManagementService: WebhookManagementService;
}
/**
* Checks active events in configurations and updates webhooks in the API if needed.
*/
export const syncWebhookStatus = async ({
smtpConfigurationService,
sendgridConfigurationService,
webhookManagementService,
}: SyncWebhooksStatusArgs) => {
logger.debug("Pulling current webhook status from the API");
const oldStatuses = await webhookManagementService.getWebhooksStatus();
logger.debug("Generate expected webhook status based on current configurations");
// API requests can be triggered if not cached yet
const [activeSmtpConfigurations, activeSendgridConfigurations] = await Promise.all([
smtpConfigurationService.getConfigurations(),
sendgridConfigurationService.getConfigurations(),
]);
const newStatuses = getWebhookStatusesFromConfigurations({
smtpConfigurations: activeSmtpConfigurations,
sendgridConfigurations: activeSendgridConfigurations,
});
logger.debug("Update webhooks in the API if needed");
const apiMutationPromises = Object.keys(newStatuses).map(async (key) => {
const webhook = key as AppWebhook;
if (newStatuses[webhook] === oldStatuses[webhook]) {
// Webhook status is already up to date
return;
}
if (newStatuses[webhook]) {
logger.debug(`Creating webhook ${webhook}`);
return webhookManagementService.createWebhook({ webhook });
} else {
logger.debug(`Deleting webhook ${webhook}`);
return webhookManagementService.deleteWebhook({ webhook });
}
});
await Promise.all(apiMutationPromises);
logger.debug("Webhooks status synchronized");
};

View file

@ -0,0 +1,153 @@
import { vi, expect, describe, it, afterEach } from "vitest";
import { WebhookManagementService } from "./webhook-management-service";
import { Client } from "urql";
import * as operationExports from "./api-operations";
import { WebhookEventTypeAsyncEnum } from "../../../generated/graphql";
import { invoiceSentWebhook } from "../../pages/api/webhooks/invoice-sent";
import { orderCancelledWebhook } from "../../pages/api/webhooks/order-cancelled";
const mockSaleorApiUrl = "https://demo.saleor.io/graphql/";
describe("WebhookManagementService", function () {
const mockedClient = {} as Client;
afterEach(() => {
vi.resetAllMocks();
});
it("API should be called, when getWebhooks is used", async () => {
const webhookManagementService = new WebhookManagementService(
"https://example.com",
mockedClient
);
const fetchAppWebhooksMock = vi.spyOn(operationExports, "fetchAppWebhooks").mockResolvedValue([
{
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
id: "1",
isActive: true,
name: invoiceSentWebhook.name,
},
]);
const webhookData = await webhookManagementService.getWebhooks();
expect(webhookData).toStrictEqual([
{
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
id: "1",
isActive: true,
name: invoiceSentWebhook.name,
},
]);
expect(fetchAppWebhooksMock).toBeCalledTimes(1);
});
it("Webhook statuses should be active, when whists in API and active", async () => {
const webhookManagementService = new WebhookManagementService(
"https://example.com",
mockedClient
);
const fetchAppWebhooksMock = vi.spyOn(operationExports, "fetchAppWebhooks").mockResolvedValue([
{
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
id: "1",
isActive: true,
name: invoiceSentWebhook.name,
},
{
asyncEvents: [
{ eventType: WebhookEventTypeAsyncEnum.OrderCancelled, name: "Order cancelled" },
],
id: "2",
isActive: false,
name: orderCancelledWebhook.name,
},
]);
const statuses = await webhookManagementService.getWebhooksStatus();
expect(statuses).toStrictEqual({
invoiceSentWebhook: true,
notifyWebhook: false,
orderCancelledWebhook: false,
orderConfirmedWebhook: false,
orderCreatedWebhook: false,
orderFulfilledWebhook: false,
orderFullyPaidWebhook: false,
});
expect(fetchAppWebhooksMock).toBeCalledTimes(1);
});
it("Webhook should be created using the API, when requested", async () => {
const webhookManagementService = new WebhookManagementService(
"https://example.com",
mockedClient
);
const createAppWebhookMock = vi.spyOn(operationExports, "createAppWebhook").mockResolvedValue({
id: "1",
isActive: true,
name: invoiceSentWebhook.name,
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
});
await webhookManagementService.createWebhook({
webhook: "invoiceSentWebhook",
});
expect(createAppWebhookMock).toBeCalledTimes(1);
// Values are taken from webhook definition
expect(createAppWebhookMock).toBeCalledWith({
client: mockedClient,
variables: {
asyncEvents: ["INVOICE_SENT"],
isActive: true,
name: invoiceSentWebhook.name,
targetUrl: "https://example.com/api/webhooks/invoice-sent",
query:
"subscription InvoiceSent { event { ...InvoiceSentWebhookPayload }}fragment InvoiceSentWebhookPayload on InvoiceSent { invoice { id message externalUrl url order { id } } order { ...OrderDetails }}fragment OrderDetails on Order { id number userEmail channel { slug } user { email firstName lastName } billingAddress { streetAddress1 city postalCode country { country } } shippingAddress { streetAddress1 city postalCode country { country } } lines { id productName variantName quantity thumbnail { url alt } unitPrice { gross { currency amount } } totalPrice { gross { currency amount } } } subtotal { gross { amount currency } } shippingPrice { gross { amount currency } } total { gross { amount currency } }}",
},
});
});
it("Webhook should be deleted using the API, when requested", async () => {
const webhookManagementService = new WebhookManagementService(
"https://example.com",
mockedClient
);
vi.spyOn(operationExports, "fetchAppWebhooks").mockResolvedValue([
{
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
id: "1",
isActive: true,
name: invoiceSentWebhook.name,
},
{
asyncEvents: [
{ eventType: WebhookEventTypeAsyncEnum.OrderCancelled, name: "Order cancelled" },
],
id: "2",
isActive: false,
name: orderCancelledWebhook.name,
},
]);
const deleteAppWebhookMock = vi.spyOn(operationExports, "deleteAppWebhook").mockResolvedValue();
await webhookManagementService.deleteWebhook({
webhook: "invoiceSentWebhook",
});
expect(deleteAppWebhookMock).toBeCalledTimes(1);
// Values are taken from webhook definition
expect(deleteAppWebhookMock).toBeCalledWith({
client: mockedClient,
id: "1",
});
});
});

View file

@ -0,0 +1,113 @@
import { invoiceSentWebhook } from "../../pages/api/webhooks/invoice-sent";
import { orderCancelledWebhook } from "../../pages/api/webhooks/order-cancelled";
import { orderConfirmedWebhook } from "../../pages/api/webhooks/order-confirmed";
import { orderCreatedWebhook } from "../../pages/api/webhooks/order-created";
import { orderFulfilledWebhook } from "../../pages/api/webhooks/order-fulfilled";
import { orderFullyPaidWebhook } from "../../pages/api/webhooks/order-fully-paid";
import { Client } from "urql";
import { createAppWebhook, deleteAppWebhook, fetchAppWebhooks } from "./api-operations";
import { notifyWebhook } from "../../pages/api/webhooks/notify";
import { MessageEventTypes } from "../event-handlers/message-event-types";
import { createLogger } from "@saleor/apps-shared";
import { WebhookEventTypeAsyncEnum } from "../../../generated/graphql";
export const AppWebhooks = {
orderCreatedWebhook,
orderFulfilledWebhook,
orderConfirmedWebhook,
orderCancelledWebhook,
orderFullyPaidWebhook,
invoiceSentWebhook,
notifyWebhook,
};
export type AppWebhook = keyof typeof AppWebhooks;
export const eventToWebhookMapping: Record<MessageEventTypes, AppWebhook> = {
ACCOUNT_CONFIRMATION: "notifyWebhook",
ACCOUNT_DELETE: "notifyWebhook",
ACCOUNT_PASSWORD_RESET: "notifyWebhook",
ACCOUNT_CHANGE_EMAIL_REQUEST: "notifyWebhook",
ACCOUNT_CHANGE_EMAIL_CONFIRM: "notifyWebhook",
INVOICE_SENT: "invoiceSentWebhook",
ORDER_CANCELLED: "orderCancelledWebhook",
ORDER_CONFIRMED: "orderConfirmedWebhook",
ORDER_CREATED: "orderCreatedWebhook",
ORDER_FULFILLED: "orderFulfilledWebhook",
ORDER_FULLY_PAID: "orderFullyPaidWebhook",
};
const logger = createLogger({
name: "WebhookManagementService",
});
export class WebhookManagementService {
constructor(private appBaseUrl: string, private client: Client) {}
// Returns list of webhooks registered for the App in the Saleor instance
public async getWebhooks() {
logger.debug("Fetching webhooks");
return await fetchAppWebhooks({ client: this.client });
}
/**
* Returns a dictionary with webhooks status.
* Status equal to true means that webhook is created and active.
*/
public async getWebhooksStatus() {
logger.debug("Fetching webhooks status");
const webhooks = await this.getWebhooks();
return Object.fromEntries(
Object.keys(AppWebhooks).map((webhook) => {
const webhookData = webhooks.find(
(w) => w.name === AppWebhooks[webhook as AppWebhook].name
);
return [webhook as AppWebhook, Boolean(webhookData?.isActive)];
})
);
}
public async createWebhook({ webhook }: { webhook: AppWebhook }) {
const webhookManifest = AppWebhooks[webhook].getWebhookManifest(this.appBaseUrl);
const asyncWebhooks = webhookManifest.asyncEvents;
if (!asyncWebhooks?.length) {
logger.warn(`Webhook ${webhook} has no async events`);
throw new Error("Only the webhooks with async events can be registered");
}
await createAppWebhook({
client: this.client,
variables: {
asyncEvents: asyncWebhooks as WebhookEventTypeAsyncEnum[],
isActive: true,
name: webhookManifest.name,
targetUrl: webhookManifest.targetUrl,
// Override empty queries to handle NOTIFY webhook
query: webhookManifest.query === "{}" ? undefined : webhookManifest.query,
},
});
}
public async deleteWebhook({ webhook }: { webhook: AppWebhook }): Promise<void> {
logger.debug(`Deleting webhook ${webhook}`);
logger.debug(`Fetching existing webhooks`);
const webhookData = await this.getWebhooks();
const id = webhookData.find((w) => w.name === AppWebhooks[webhook].name)?.id;
if (!id) {
logger.error(`Webhook ${AppWebhooks[webhook].name} was not registered yet`);
throw new Error(`Webhook ${AppWebhooks[webhook].name} was not registered yet`);
}
logger.debug(`Running delete mutation`);
await deleteAppWebhook({
client: this.client,
id,
});
}
}

View file

@ -2,12 +2,6 @@ import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { AppManifest } from "@saleor/app-sdk/types";
import packageJson from "../../../package.json";
import { orderCreatedWebhook } from "./webhooks/order-created";
import { orderFulfilledWebhook } from "./webhooks/order-fulfilled";
import { orderConfirmedWebhook } from "./webhooks/order-confirmed";
import { orderCancelledWebhook } from "./webhooks/order-cancelled";
import { orderFullyPaidWebhook } from "./webhooks/order-fully-paid";
import { invoiceSentWebhook } from "./webhooks/invoice-sent";
export default createManifestHandler({
async manifestFactory(context) {
@ -18,14 +12,6 @@ export default createManifestHandler({
permissions: ["MANAGE_ORDERS", "MANAGE_USERS"],
id: "saleor.app.emails-and-messages",
version: packageJson.version,
webhooks: [
orderCreatedWebhook.getWebhookManifest(context.appBaseUrl),
orderFulfilledWebhook.getWebhookManifest(context.appBaseUrl),
orderConfirmedWebhook.getWebhookManifest(context.appBaseUrl),
orderCancelledWebhook.getWebhookManifest(context.appBaseUrl),
orderFullyPaidWebhook.getWebhookManifest(context.appBaseUrl),
invoiceSentWebhook.getWebhookManifest(context.appBaseUrl),
],
extensions: [
/**
* Optionally, extend Dashboard with custom UIs

View file

@ -1,9 +1,6 @@
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
import { saleorApp } from "../../saleor-app";
import { logger, createGraphQLClient } from "@saleor/apps-shared";
import { getBaseUrl } from "../../lib/get-base-url";
import { registerNotifyWebhook } from "../../lib/register-notify-webhook";
const allowedUrlsPattern = process.env.ALLOWED_DOMAIN_PATTERN;
@ -24,19 +21,4 @@ export default createAppRegisterHandler({
return true;
},
],
onAuthAplSaved: async (request, ctx) => {
// Subscribe to Notify using the mutation since it does not use subscriptions and can't be subscribed via manifest
logger.debug("onAuthAplSaved executing");
const baseUrl = getBaseUrl(request.headers);
const client = createGraphQLClient({
saleorApiUrl: ctx.authData.saleorApiUrl,
token: ctx.authData.token,
});
await registerNotifyWebhook({
client: client,
baseUrl: baseUrl,
});
logger.debug("Webhook registered");
},
});