Move bulk of webhook logic to plugin (#4866)

* Move bulk of webhook logic to plugin

* Re-enable cleanup task

* cron tasks
This commit is contained in:
Tom Moor
2023-02-12 19:28:11 -05:00
committed by GitHub
parent 7895ee207c
commit 60101c507a
30 changed files with 74 additions and 67 deletions

View File

@@ -10,7 +10,6 @@ import {
TeamIcon, TeamIcon,
BeakerIcon, BeakerIcon,
BuildingBlocksIcon, BuildingBlocksIcon,
WebhooksIcon,
SettingsIcon, SettingsIcon,
ExportIcon, ExportIcon,
ImportIcon, ImportIcon,
@@ -32,7 +31,6 @@ import Security from "~/scenes/Settings/Security";
import SelfHosted from "~/scenes/Settings/SelfHosted"; import SelfHosted from "~/scenes/Settings/SelfHosted";
import Shares from "~/scenes/Settings/Shares"; import Shares from "~/scenes/Settings/Shares";
import Tokens from "~/scenes/Settings/Tokens"; import Tokens from "~/scenes/Settings/Tokens";
import Webhooks from "~/scenes/Settings/Webhooks";
import Zapier from "~/scenes/Settings/Zapier"; import Zapier from "~/scenes/Settings/Zapier";
import GoogleIcon from "~/components/Icons/GoogleIcon"; import GoogleIcon from "~/components/Icons/GoogleIcon";
import ZapierIcon from "~/components/Icons/ZapierIcon"; import ZapierIcon from "~/components/Icons/ZapierIcon";
@@ -172,14 +170,6 @@ const useSettingsConfig = () => {
icon: plugin.icon, icon: plugin.icon,
} as ConfigItem; } as ConfigItem;
}), }),
Webhooks: {
name: t("Webhooks"),
path: "/settings/webhooks",
component: Webhooks,
enabled: can.createWebhookSubscription,
group: t("Integrations"),
icon: WebhooksIcon,
},
SelfHosted: { SelfHosted: {
name: t("Self Hosted"), name: t("Self Hosted"),
path: integrationSettingsPath("self-hosted"), path: integrationSettingsPath("self-hosted"),

View File

@@ -13,7 +13,7 @@
"start": "node ./build/server/index.js", "start": "node ./build/server/index.js",
"dev": "NODE_ENV=development yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"", "dev": "NODE_ENV=development yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"",
"dev:watch": "nodemon --exec \"yarn build:server && yarn dev\" -e js,ts,tsx --ignore build/ --ignore app/ --ignore shared/editor", "dev:watch": "nodemon --exec \"yarn build:server && yarn dev\" -e js,ts,tsx --ignore build/ --ignore app/ --ignore shared/editor",
"lint": "eslint app server shared", "lint": "eslint app server shared plugins",
"prepare": "husky install", "prepare": "husky install",
"postinstall": "rimraf node_modules/prosemirror-view/dist/index.d.ts", "postinstall": "rimraf node_modules/prosemirror-view/dist/index.d.ts",
"heroku-postbuild": "yarn build && yarn db:migrate", "heroku-postbuild": "yarn build && yarn db:migrate",

View File

@@ -0,0 +1 @@
export { WebhooksIcon as default } from "outline-icons";

View File

@@ -0,0 +1,5 @@
{
"name": "Webhooks",
"description": "Adds HTTP webhooks for various events.",
"requiredEnvVars": []
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../../server/.babelrc"
}

View File

@@ -4,10 +4,10 @@ import { ValidationError } from "@server/errors";
import auth from "@server/middlewares/authentication"; import auth from "@server/middlewares/authentication";
import { WebhookSubscription, Event } from "@server/models"; import { WebhookSubscription, Event } from "@server/models";
import { authorize } from "@server/policies"; import { authorize } from "@server/policies";
import { presentWebhookSubscription } from "@server/presenters"; import pagination from "@server/routes/api/middlewares/pagination";
import { WebhookSubscriptionEvent, APIContext } from "@server/types"; import { WebhookSubscriptionEvent, APIContext } from "@server/types";
import { assertArray, assertPresent, assertUuid } from "@server/validation"; import { assertArray, assertPresent, assertUuid } from "@server/validation";
import pagination from "./middlewares/pagination"; import presentWebhookSubscription from "../presenters/webhookSubscription";
const router = new Router(); const router = new Router();

View File

@@ -1,7 +1,7 @@
import { WebhookSubscription } from "@server/models"; import { WebhookSubscription } from "@server/models";
import BaseProcessor from "@server/queues/processors/BaseProcessor";
import { Event } from "@server/types"; import { Event } from "@server/types";
import DeliverWebhookTask from "../tasks/DeliverWebhookTask"; import DeliverWebhookTask from "../tasks/DeliverWebhookTask";
import BaseProcessor from "./BaseProcessor";
export default class WebhookProcessor extends BaseProcessor { export default class WebhookProcessor extends BaseProcessor {
static applicableEvents: ["*"] = ["*"]; static applicableEvents: ["*"] = ["*"];

View File

@@ -2,11 +2,16 @@ import { subDays } from "date-fns";
import { Op } from "sequelize"; import { Op } from "sequelize";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import { WebhookDelivery } from "@server/models"; import { WebhookDelivery } from "@server/models";
import BaseTask, { TaskPriority } from "./BaseTask"; import BaseTask, {
TaskPriority,
TaskSchedule,
} from "@server/queues/tasks/BaseTask";
type Props = void; type Props = void;
export default class CleanupWebhookDeliveriesTask extends BaseTask<Props> { export default class CleanupWebhookDeliveriesTask extends BaseTask<Props> {
static cron = TaskSchedule.Daily;
public async perform(_: Props) { public async perform(_: Props) {
Logger.info("task", `Deleting WebhookDeliveries older than one week…`); Logger.info("task", `Deleting WebhookDeliveries older than one week…`);
const count = await WebhookDelivery.unscoped().destroy({ const count = await WebhookDelivery.unscoped().destroy({

View File

@@ -34,15 +34,13 @@ import {
presentStar, presentStar,
presentTeam, presentTeam,
presentUser, presentUser,
presentWebhook,
presentWebhookSubscription,
presentView, presentView,
presentShare, presentShare,
presentMembership, presentMembership,
presentGroupMembership, presentGroupMembership,
presentCollectionGroupMembership, presentCollectionGroupMembership,
} from "@server/presenters"; } from "@server/presenters";
import { WebhookPayload } from "@server/presenters/webhook"; import BaseTask from "@server/queues/tasks/BaseTask";
import { import {
CollectionEvent, CollectionEvent,
CollectionGroupEvent, CollectionGroupEvent,
@@ -62,7 +60,8 @@ import {
ViewEvent, ViewEvent,
WebhookSubscriptionEvent, WebhookSubscriptionEvent,
} from "@server/types"; } from "@server/types";
import BaseTask from "./BaseTask"; import presentWebhook, { WebhookPayload } from "../presenters/webhook";
import presentWebhookSubscription from "../presenters/webhookSubscription";
function assertUnreachable(event: never) { function assertUnreachable(event: never) {
Logger.warn(`DeliverWebhookTask did not handle ${(event as any).name}`); Logger.warn(`DeliverWebhookTask did not handle ${(event as any).name}`);

View File

@@ -23,8 +23,6 @@ import presentSubscription from "./subscription";
import presentTeam from "./team"; import presentTeam from "./team";
import presentUser from "./user"; import presentUser from "./user";
import presentView from "./view"; import presentView from "./view";
import presentWebhook from "./webhook";
import presentWebhookSubscription from "./webhookSubscription";
export { export {
presentApiKey, presentApiKey,
@@ -52,6 +50,4 @@ export {
presentTeam, presentTeam,
presentUser, presentUser,
presentView, presentView,
presentWebhook,
presentWebhookSubscription,
}; };

View File

@@ -8,7 +8,17 @@ export enum TaskPriority {
High = 10, High = 10,
} }
export enum TaskSchedule {
Daily = "daily",
Hourly = "hourly",
}
export default abstract class BaseTask<T> { export default abstract class BaseTask<T> {
/**
* An optional schedule for this task to be run automatically.
*/
static cron: TaskSchedule | undefined;
/** /**
* Schedule this task type to be processed asyncronously by a worker. * Schedule this task type to be processed asyncronously by a worker.
* *

View File

@@ -3,13 +3,15 @@ import { Op } from "sequelize";
import documentPermanentDeleter from "@server/commands/documentPermanentDeleter"; import documentPermanentDeleter from "@server/commands/documentPermanentDeleter";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import { Document } from "@server/models"; import { Document } from "@server/models";
import BaseTask, { TaskPriority } from "./BaseTask"; import BaseTask, { TaskPriority, TaskSchedule } from "./BaseTask";
type Props = { type Props = {
limit: number; limit: number;
}; };
export default class CleanupDeletedDocumentsTask extends BaseTask<Props> { export default class CleanupDeletedDocumentsTask extends BaseTask<Props> {
static cron = TaskSchedule.Daily;
public async perform({ limit }: Props) { public async perform({ limit }: Props) {
Logger.info( Logger.info(
"task", "task",

View File

@@ -3,13 +3,15 @@ import { Op } from "sequelize";
import teamPermanentDeleter from "@server/commands/teamPermanentDeleter"; import teamPermanentDeleter from "@server/commands/teamPermanentDeleter";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import { Team } from "@server/models"; import { Team } from "@server/models";
import BaseTask, { TaskPriority } from "./BaseTask"; import BaseTask, { TaskPriority, TaskSchedule } from "./BaseTask";
type Props = { type Props = {
limit: number; limit: number;
}; };
export default class CleanupDeletedTeamsTask extends BaseTask<Props> { export default class CleanupDeletedTeamsTask extends BaseTask<Props> {
static cron = TaskSchedule.Daily;
public async perform({ limit }: Props) { public async perform({ limit }: Props) {
Logger.info( Logger.info(
"task", "task",

View File

@@ -1,13 +1,15 @@
import { Op } from "sequelize"; import { Op } from "sequelize";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import { Attachment } from "@server/models"; import { Attachment } from "@server/models";
import BaseTask, { TaskPriority } from "./BaseTask"; import BaseTask, { TaskPriority, TaskSchedule } from "./BaseTask";
type Props = { type Props = {
limit: number; limit: number;
}; };
export default class CleanupExpiredAttachmentsTask extends BaseTask<Props> { export default class CleanupExpiredAttachmentsTask extends BaseTask<Props> {
static cron = TaskSchedule.Daily;
public async perform({ limit }: Props) { public async perform({ limit }: Props) {
Logger.info("task", `Deleting expired attachments…`); Logger.info("task", `Deleting expired attachments…`);
const attachments = await Attachment.unscoped().findAll({ const attachments = await Attachment.unscoped().findAll({

View File

@@ -3,13 +3,15 @@ import { Op } from "sequelize";
import { FileOperationState } from "@shared/types"; import { FileOperationState } from "@shared/types";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import { FileOperation } from "@server/models"; import { FileOperation } from "@server/models";
import BaseTask, { TaskPriority } from "./BaseTask"; import BaseTask, { TaskPriority, TaskSchedule } from "./BaseTask";
type Props = { type Props = {
limit: number; limit: number;
}; };
export default class CleanupExpiredFileOperationsTask extends BaseTask<Props> { export default class CleanupExpiredFileOperationsTask extends BaseTask<Props> {
static cron = TaskSchedule.Daily;
public async perform({ limit }: Props) { public async perform({ limit }: Props) {
Logger.info("task", `Expiring file operations older than 15 days…`); Logger.info("task", `Expiring file operations older than 15 days…`);
const fileOperations = await FileOperation.unscoped().findAll({ const fileOperations = await FileOperation.unscoped().findAll({

View File

@@ -4,11 +4,13 @@ import { sequelize } from "@server/database/sequelize";
import InviteReminderEmail from "@server/emails/templates/InviteReminderEmail"; import InviteReminderEmail from "@server/emails/templates/InviteReminderEmail";
import { User } from "@server/models"; import { User } from "@server/models";
import { UserFlag } from "@server/models/User"; import { UserFlag } from "@server/models/User";
import BaseTask, { TaskPriority } from "./BaseTask"; import BaseTask, { TaskPriority, TaskSchedule } from "./BaseTask";
type Props = undefined; type Props = undefined;
export default class InviteReminderTask extends BaseTask<Props> { export default class InviteReminderTask extends BaseTask<Props> {
static cron = TaskSchedule.Daily;
public async perform() { public async perform() {
const users = await User.scope("invited").findAll({ const users = await User.scope("invited").findAll({
attributes: ["id"], attributes: ["id"],

View File

@@ -3,12 +3,7 @@ import { Context } from "koa";
import Router from "koa-router"; import Router from "koa-router";
import env from "@server/env"; import env from "@server/env";
import { AuthenticationError } from "@server/errors"; import { AuthenticationError } from "@server/errors";
import CleanupDeletedDocumentsTask from "@server/queues/tasks/CleanupDeletedDocumentsTask"; import tasks from "@server/queues/tasks";
import CleanupDeletedTeamsTask from "@server/queues/tasks/CleanupDeletedTeamsTask";
import CleanupExpiredAttachmentsTask from "@server/queues/tasks/CleanupExpiredAttachmentsTask";
import CleanupExpiredFileOperationsTask from "@server/queues/tasks/CleanupExpiredFileOperationsTask";
import CleanupWebhookDeliveriesTask from "@server/queues/tasks/CleanupWebhookDeliveriesTask";
import InviteReminderTask from "@server/queues/tasks/InviteReminderTask";
const router = new Router(); const router = new Router();
@@ -34,17 +29,12 @@ const cronHandler = async (ctx: Context) => {
throw AuthenticationError("Invalid secret token"); throw AuthenticationError("Invalid secret token");
} }
await CleanupDeletedDocumentsTask.schedule({ limit }); for (const name in tasks) {
const TaskClass = tasks[name];
await CleanupExpiredFileOperationsTask.schedule({ limit }); if (TaskClass.cron) {
await TaskClass.schedule({ limit });
await CleanupExpiredAttachmentsTask.schedule({ limit }); }
}
await CleanupDeletedTeamsTask.schedule({ limit });
await CleanupWebhookDeliveriesTask.schedule({ limit });
await InviteReminderTask.schedule();
ctx.body = { ctx.body = {
success: true, success: true,

View File

@@ -32,7 +32,6 @@ import subscriptions from "./subscriptions";
import team from "./team"; import team from "./team";
import users from "./users"; import users from "./users";
import views from "./views"; import views from "./views";
import webhookSubscriptions from "./webhookSubscriptions";
const api = new Koa<AppState, AppContext>(); const api = new Koa<AppState, AppContext>();
const router = new Router(); const router = new Router();
@@ -82,7 +81,6 @@ router.use("/", attachments.routes());
router.use("/", utils.routes()); router.use("/", utils.routes());
router.use("/", groups.routes()); router.use("/", groups.routes());
router.use("/", fileOperationsRoute.routes()); router.use("/", fileOperationsRoute.routes());
router.use("/", webhookSubscriptions.routes());
if (env.ENVIRONMENT === "development") { if (env.ENVIRONMENT === "development") {
router.use("/", developer.routes()); router.use("/", developer.routes());

View File

@@ -313,7 +313,6 @@
"Shared Links": "Shared Links", "Shared Links": "Shared Links",
"Import": "Import", "Import": "Import",
"Integrations": "Integrations", "Integrations": "Integrations",
"Webhooks": "Webhooks",
"Self Hosted": "Self Hosted", "Self Hosted": "Self Hosted",
"Google Analytics": "Google Analytics", "Google Analytics": "Google Analytics",
"Show path to document": "Show path to document", "Show path to document": "Show path to document",
@@ -671,22 +670,6 @@
"Active": "Active", "Active": "Active",
"Everyone": "Everyone", "Everyone": "Everyone",
"Admins": "Admins", "Admins": "Admins",
"Are you sure you want to delete the {{ name }} webhook?": "Are you sure you want to delete the {{ name }} webhook?",
"Webhook updated": "Webhook updated",
"Update": "Update",
"Updating": "Updating",
"Provide a descriptive name for this webhook and the URL we should send a POST request to when matching events are created.": "Provide a descriptive name for this webhook and the URL we should send a POST request to when matching events are created.",
"A memorable identifer": "A memorable identifer",
"URL": "URL",
"Signing secret": "Signing secret",
"Subscribe to all events, groups, or individual events. We recommend only subscribing to the minimum amount of events that your application needs to function.": "Subscribe to all events, groups, or individual events. We recommend only subscribing to the minimum amount of events that your application needs to function.",
"All events": "All events",
"All {{ groupName }} events": "All {{ groupName }} events",
"Delete webhook": "Delete webhook",
"Disabled": "Disabled",
"Subscribed events": "Subscribed events",
"Edit webhook": "Edit webhook",
"Webhook created": "Webhook created",
"Settings saved": "Settings saved", "Settings saved": "Settings saved",
"Logo updated": "Logo updated", "Logo updated": "Logo updated",
"Unable to upload new logo": "Unable to upload new logo", "Unable to upload new logo": "Unable to upload new logo",
@@ -770,6 +753,7 @@
"Settings that impact the access, security, and content of your knowledge base.": "Settings that impact the access, security, and content of your knowledge base.", "Settings that impact the access, security, and content of your knowledge base.": "Settings that impact the access, security, and content of your knowledge base.",
"Allow members to sign-in with {{ authProvider }}": "Allow members to sign-in with {{ authProvider }}", "Allow members to sign-in with {{ authProvider }}": "Allow members to sign-in with {{ authProvider }}",
"Connected": "Connected", "Connected": "Connected",
"Disabled": "Disabled",
"Allow members to sign-in using their email address": "Allow members to sign-in using their email address", "Allow members to sign-in using their email address": "Allow members to sign-in using their email address",
"The server must have SMTP configured to enable this setting": "The server must have SMTP configured to enable this setting", "The server must have SMTP configured to enable this setting": "The server must have SMTP configured to enable this setting",
"Access": "Access", "Access": "Access",
@@ -793,9 +777,6 @@
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.", "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens", "Tokens": "Tokens",
"Create a token": "Create a token", "Create a token": "Create a token",
"New webhook": "New webhook",
"Webhooks can be used to notify your application when events happen in {{appName}}. Events are sent as a https request with a JSON payload in near real-time.": "Webhooks can be used to notify your application when events happen in {{appName}}. Events are sent as a https request with a JSON payload in near real-time.",
"Create a webhook": "Create a webhook",
"Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.": "Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.", "Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.": "Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.",
"Your are creating a new workspace using your current account — <em>{{email}}</em>": "Your are creating a new workspace using your current account — <em>{{email}}</em>", "Your are creating a new workspace using your current account — <em>{{email}}</em>": "Your are creating a new workspace using your current account — <em>{{email}}</em>",
"Workspace name": "Workspace name", "Workspace name": "Workspace name",
@@ -838,5 +819,24 @@
"Post to Channel": "Post to Channel", "Post to Channel": "Post to Channel",
"This is what we found for \"{{ term }}\"": "This is what we found for \"{{ term }}\"", "This is what we found for \"{{ term }}\"": "This is what we found for \"{{ term }}\"",
"No results for \"{{ term }}\"": "No results for \"{{ term }}\"", "No results for \"{{ term }}\"": "No results for \"{{ term }}\"",
"Are you sure you want to delete the {{ name }} webhook?": "Are you sure you want to delete the {{ name }} webhook?",
"Webhook updated": "Webhook updated",
"Update": "Update",
"Updating": "Updating",
"Provide a descriptive name for this webhook and the URL we should send a POST request to when matching events are created.": "Provide a descriptive name for this webhook and the URL we should send a POST request to when matching events are created.",
"A memorable identifer": "A memorable identifer",
"URL": "URL",
"Signing secret": "Signing secret",
"Subscribe to all events, groups, or individual events. We recommend only subscribing to the minimum amount of events that your application needs to function.": "Subscribe to all events, groups, or individual events. We recommend only subscribing to the minimum amount of events that your application needs to function.",
"All events": "All events",
"All {{ groupName }} events": "All {{ groupName }} events",
"Delete webhook": "Delete webhook",
"Subscribed events": "Subscribed events",
"Edit webhook": "Edit webhook",
"Webhook created": "Webhook created",
"Webhooks": "Webhooks",
"New webhook": "New webhook",
"Webhooks can be used to notify your application when events happen in {{appName}}. Events are sent as a https request with a JSON payload in near real-time.": "Webhooks can be used to notify your application when events happen in {{appName}}. Events are sent as a https request with a JSON payload in near real-time.",
"Create a webhook": "Create a webhook",
"Uploading": "Uploading" "Uploading": "Uploading"
} }