chore: Plugin registration (#6623)

* first pass

* test

* test

* priority

* Reduce boilerplate further

* Update server/utils/PluginManager.ts

Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com>

* fix: matchesNode error in destroyed editor transaction

* fix: Individual imported files do not display source correctly in 'Insights'

* chore: Add sleep before Slack notification

* docs

* fix: Error logged about missing plugin.json

* Remove email template glob

---------

Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com>
This commit is contained in:
Tom Moor
2024-03-08 21:32:05 -07:00
committed by GitHub
parent f3334cedb2
commit f9a11a28d8
43 changed files with 400 additions and 276 deletions

View File

@@ -1,5 +1,6 @@
{
"id": "azure",
"name": "Microsoft",
"description": "Adds a Microsoft Azure authentication provider.",
"requiredEnvVars": ["AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"]
"priority": 20,
"description": "Adds a Microsoft Azure authentication provider."
}

View File

@@ -16,10 +16,10 @@ import {
getTeamFromContext,
getClientFromContext,
} from "@server/utils/passport";
import config from "../../plugin.json";
import env from "../env";
const router = new Router();
const providerName = "azure";
const scopes: string[] = [];
if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
@@ -109,7 +109,7 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
avatarUrl: profile.picture,
},
authenticationProvider: {
name: providerName,
name: config.id,
providerId: profile.tid,
},
authentication: {
@@ -127,13 +127,11 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
}
);
passport.use(strategy);
router.get(
"azure",
passport.authenticate(providerName, { prompt: "select_account" })
config.id,
passport.authenticate(config.id, { prompt: "select_account" })
);
router.get("azure.callback", passportMiddleware(providerName));
router.get(`${config.id}.callback`, passportMiddleware(config.id));
}
export default router;

View File

@@ -0,0 +1,9 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager";
import config from "../plugin.json";
import router from "./auth/azure";
import env from "./env";
PluginManager.register(PluginType.AuthProvider, router, {
...config,
enabled: !!env.AZURE_CLIENT_ID && !!env.AZURE_CLIENT_SECRET,
});

View File

@@ -1,4 +1,5 @@
{
"id": "email",
"name": "Email",
"description": "Adds an email magic link authentication provider."
}

View File

@@ -0,0 +1,9 @@
import env from "@server/env";
import { PluginManager, PluginType } from "@server/utils/PluginManager";
import config from "../plugin.json";
import router from "./auth/email";
PluginManager.register(PluginType.AuthProvider, router, {
...config,
enabled: (!!env.SMTP_HOST && !!env.SMTP_USERNAME) || env.isDevelopment,
});

View File

@@ -1,5 +1,6 @@
{
"id": "google",
"name": "Google",
"description": "Adds a Google authentication provider.",
"requiredEnvVars": ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"]
"priority": 10,
"description": "Adds a Google authentication provider."
}

View File

@@ -18,10 +18,10 @@ import {
getTeamFromContext,
getClientFromContext,
} from "@server/utils/passport";
import config from "../../plugin.json";
import env from "../env";
const router = new Router();
const providerName = "google";
const scopes = [
"https://www.googleapis.com/auth/userinfo.profile",
@@ -42,7 +42,7 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
{
clientID: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
callbackURL: `${env.URL}/auth/google.callback`,
callbackURL: `${env.URL}/auth/${config.id}.callback`,
passReqToCallback: true,
// @ts-expect-error StateStore
store: new StateStore(),
@@ -110,7 +110,7 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
avatarUrl,
},
authenticationProvider: {
name: providerName,
name: config.id,
providerId: domain ?? "",
},
authentication: {
@@ -131,14 +131,13 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
);
router.get(
"google",
passport.authenticate(providerName, {
config.id,
passport.authenticate(config.id, {
accessType: "offline",
prompt: "select_account consent",
})
);
router.get("google.callback", passportMiddleware(providerName));
router.get(`${config.id}.callback`, passportMiddleware(config.id));
}
export default router;

View File

@@ -0,0 +1,9 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager";
import config from "../plugin.json";
import router from "./auth/google";
import env from "./env";
PluginManager.register(PluginType.AuthProvider, router, {
...config,
enabled: !!env.GOOGLE_CLIENT_ID && !!env.GOOGLE_CLIENT_SECRET,
});

View File

@@ -1,5 +0,0 @@
{
"name": "Iframely",
"description": "Integrate Iframely to enable unfurling of arbitrary urls",
"requiredEnvVars": ["IFRAMELY_API_KEY"]
}

View File

@@ -1,3 +1,4 @@
import type { Unfurl } from "@shared/types";
import { Day } from "@shared/utils/time";
import { InternalError } from "@server/errors";
import Logger from "@server/logging/Logger";
@@ -33,7 +34,7 @@ class Iframely {
}
}
private static async fetch(url: string, type = "oembed") {
public static async fetch(url: string, type = "oembed") {
const res = await fetch(
`${this.apiUrl}/${type}?url=${encodeURIComponent(url)}&api_key=${
this.apiKey
@@ -55,20 +56,19 @@ class Iframely {
}
/**
* Fetches the preview data for the given url
* using Iframely oEmbed API
* Fetches the preview data for the given url using Iframely oEmbed API
*
* @param url
* @returns Preview data for the url
*/
public static async get(url: string) {
public static async get(url: string): Promise<Unfurl | false> {
try {
const cached = await this.cached(url);
const cached = await Iframely.cached(url);
if (cached) {
return cached;
}
const res = await this.fetch(url);
await this.cache(url, res);
const res = await Iframely.fetch(url);
await Iframely.cache(url, res);
return res;
} catch (err) {
throw InternalError(err);

View File

@@ -0,0 +1,15 @@
import {
PluginManager,
PluginPriority,
PluginType,
} from "@server/utils/PluginManager";
import env from "./env";
import Iframely from "./iframely";
PluginManager.register(PluginType.UnfurlProvider, Iframely.get, {
id: "iframely",
enabled: !!env.IFRAMELY_API_KEY && !!env.IFRAMELY_URL,
// Make sure this is last in the stack to be evaluated after all other unfurl providers
priority: PluginPriority.VeryLow,
});

View File

@@ -1,3 +0,0 @@
import Iframely from "./iframely";
export const unfurl = async (url: string) => Iframely.get(url);

View File

@@ -1,5 +1,6 @@
{
"id": "oidc",
"name": "OIDC",
"description": "Adds an OpenID compatible authentication provider.",
"requiredEnvVars": ["OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET", "OIDC_AUTH_URI", "OIDC_TOKEN_URI", "OIDC_USERINFO_URI"]
"priority": 30,
"description": "Adds an OpenID compatible authentication provider."
}

View File

@@ -18,10 +18,10 @@ import {
getTeamFromContext,
getClientFromContext,
} from "@server/utils/passport";
import config from "../../plugin.json";
import env from "../env";
const router = new Router();
const providerName = "oidc";
const scopes = env.OIDC_SCOPES.split(" ");
Strategy.prototype.userProfile = async function (accessToken, done) {
@@ -55,14 +55,14 @@ if (
env.OIDC_USERINFO_URI
) {
passport.use(
providerName,
config.id,
new Strategy(
{
authorizationURL: env.OIDC_AUTH_URI,
tokenURL: env.OIDC_TOKEN_URI,
clientID: env.OIDC_CLIENT_ID,
clientSecret: env.OIDC_CLIENT_SECRET,
callbackURL: `${env.URL}/auth/${providerName}.callback`,
callbackURL: `${env.URL}/auth/${config.id}.callback`,
passReqToCallback: true,
scope: env.OIDC_SCOPES,
// @ts-expect-error custom state store
@@ -134,7 +134,7 @@ if (
avatarUrl: profile.picture,
},
authenticationProvider: {
name: providerName,
name: config.id,
providerId: domain,
},
authentication: {
@@ -153,11 +153,8 @@ if (
)
);
router.get(providerName, passport.authenticate(providerName));
router.get(`${providerName}.callback`, passportMiddleware(providerName));
router.get(config.id, passport.authenticate(config.id));
router.get(`${config.id}.callback`, passportMiddleware(config.id));
}
export const name = env.OIDC_DISPLAY_NAME;
export default router;

View File

@@ -0,0 +1,16 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager";
import config from "../plugin.json";
import router from "./auth/oidc";
import env from "./env";
PluginManager.register(PluginType.AuthProvider, router, {
...config,
name: env.OIDC_DISPLAY_NAME || config.name,
enabled: !!(
env.OIDC_CLIENT_ID &&
env.OIDC_CLIENT_SECRET &&
env.OIDC_AUTH_URI &&
env.OIDC_TOKEN_URI &&
env.OIDC_USERINFO_URI
),
});

View File

@@ -1,5 +1,6 @@
{
"id": "slack",
"name": "Slack",
"description": "Adds a Slack authentication provider, support for the /outline slash command, and link unfurling.",
"requiredEnvVars": ["SLACK_CLIENT_ID", "SLACK_CLIENT_SECRET"]
"priority": 40,
"description": "Adds a Slack authentication provider, support for the /outline slash command, and link unfurling."
}

View File

@@ -0,0 +1,20 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager";
import config from "../plugin.json";
import hooks from "./api/hooks";
import router from "./auth/slack";
import env from "./env";
import SlackProcessor from "./processors/SlackProcessor";
const enabled = !!env.SLACK_CLIENT_ID && !!env.SLACK_CLIENT_SECRET;
PluginManager.register(PluginType.AuthProvider, router, {
...config,
enabled,
});
PluginManager.register(PluginType.API, hooks, {
...config,
enabled,
});
PluginManager.registerProcessor(SlackProcessor, { enabled });

View File

@@ -1,8 +0,0 @@
{
"name": "Storage",
"description": "Plugin for storing files on the local file system",
"requiredEnvVars": [
"FILE_STORAGE_UPLOAD_MAX_SIZE",
"FILE_STORAGE_LOCAL_ROOT_DIR"
]
}

View File

@@ -18,11 +18,8 @@ import FileStorage from "@server/storage/files";
import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import { getJWTPayload } from "@server/utils/jwt";
import { createRootDirForLocalStorage } from "../utils";
import * as T from "./schema";
createRootDirForLocalStorage();
const router = new Router();
router.post(

View File

@@ -0,0 +1,31 @@
import { existsSync, mkdirSync } from "fs";
import env from "@server/env";
import Logger from "@server/logging/Logger";
import { PluginManager, PluginType } from "@server/utils/PluginManager";
import router from "./api/files";
if (env.FILE_STORAGE === "local") {
const rootDir = env.FILE_STORAGE_LOCAL_ROOT_DIR;
try {
if (!existsSync(rootDir)) {
mkdirSync(rootDir, { recursive: true });
Logger.debug("utils", `Created ${rootDir} for local storage`);
}
} catch (err) {
Logger.fatal(
`Failed to create directory for local file storage at ${env.FILE_STORAGE_LOCAL_ROOT_DIR}`,
err
);
}
}
PluginManager.register(PluginType.API, router, {
id: "files",
name: "Local file storage",
description: "Plugin for storing files on the local file system",
enabled: !!(
env.FILE_STORAGE_UPLOAD_MAX_SIZE &&
env.FILE_STORAGE_LOCAL_ROOT_DIR &&
env.FILE_STORAGE === "local"
),
});

View File

@@ -1,20 +0,0 @@
import { existsSync, mkdirSync } from "fs";
import env from "@server/env";
import Logger from "@server/logging/Logger";
export const createRootDirForLocalStorage = () => {
if (env.FILE_STORAGE === "local") {
const rootDir = env.FILE_STORAGE_LOCAL_ROOT_DIR;
try {
if (!existsSync(rootDir)) {
mkdirSync(rootDir, { recursive: true });
Logger.debug("utils", `Created ${rootDir} for local storage`);
}
} catch (err) {
Logger.fatal(
`Failed to create directory for local file storage at ${env.FILE_STORAGE_LOCAL_ROOT_DIR}`,
err
);
}
}
};

View File

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

View File

@@ -0,0 +1,11 @@
import { PluginManager, PluginType } from "@server/utils/PluginManager";
import config from "../plugin.json";
import webhookSubscriptions from "./api/webhookSubscriptions";
import WebhookProcessor from "./processors/WebhookProcessor";
import CleanupWebhookDeliveriesTask from "./tasks/CleanupWebhookDeliveriesTask";
import DeliverWebhookTask from "./tasks/DeliverWebhookTask";
PluginManager.register(PluginType.API, webhookSubscriptions, config)
.registerProcessor(WebhookProcessor)
.registerTask(DeliverWebhookTask)
.registerTask(CleanupWebhookDeliveriesTask);

View File

@@ -7,12 +7,12 @@ import BaseTask, {
TaskSchedule,
} from "@server/queues/tasks/BaseTask";
type Props = void;
type Props = Record<string, never>;
export default class CleanupWebhookDeliveriesTask extends BaseTask<Props> {
static cron = TaskSchedule.Daily;
public async perform(_: Props) {
public async perform() {
Logger.info("task", `Deleting WebhookDeliveries older than one week…`);
const count = await WebhookDelivery.unscoped().destroy({
where: {