mirror of
https://github.com/outline/outline.git
synced 2025-12-30 07:19:52 -06:00
Plugin architecture (#4861)
* wip * Refactor, tasks, processors, routes loading * Move Slack settings config to plugin * Fix translations in plugins * Move Slack auth to plugin * test * Move other slack-related files into plugin * Forgot to save * refactor
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { mapValues } from "lodash";
|
||||
import {
|
||||
EmailIcon,
|
||||
ProfileIcon,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
} from "outline-icons";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
||||
import Details from "~/scenes/Settings/Details";
|
||||
import Export from "~/scenes/Settings/Export";
|
||||
import Features from "~/scenes/Settings/Features";
|
||||
@@ -29,36 +31,18 @@ import Profile from "~/scenes/Settings/Profile";
|
||||
import Security from "~/scenes/Settings/Security";
|
||||
import SelfHosted from "~/scenes/Settings/SelfHosted";
|
||||
import Shares from "~/scenes/Settings/Shares";
|
||||
import Slack from "~/scenes/Settings/Slack";
|
||||
import Tokens from "~/scenes/Settings/Tokens";
|
||||
import Webhooks from "~/scenes/Settings/Webhooks";
|
||||
import Zapier from "~/scenes/Settings/Zapier";
|
||||
import GoogleIcon from "~/components/Icons/GoogleIcon";
|
||||
import SlackIcon from "~/components/Icons/SlackIcon";
|
||||
import ZapierIcon from "~/components/Icons/ZapierIcon";
|
||||
import env from "~/env";
|
||||
import isCloudHosted from "~/utils/isCloudHosted";
|
||||
import { loadPlugins } from "~/utils/plugins";
|
||||
import { accountPreferencesPath } from "~/utils/routeHelpers";
|
||||
import useCurrentTeam from "./useCurrentTeam";
|
||||
import usePolicy from "./usePolicy";
|
||||
|
||||
type SettingsGroups = "Account" | "Team" | "Integrations";
|
||||
type SettingsPage =
|
||||
| "Profile"
|
||||
| "Notifications"
|
||||
| "Api"
|
||||
| "Details"
|
||||
| "Security"
|
||||
| "Features"
|
||||
| "Members"
|
||||
| "Groups"
|
||||
| "Shares"
|
||||
| "Import"
|
||||
| "Export"
|
||||
| "Webhooks"
|
||||
| "Slack"
|
||||
| "Zapier"
|
||||
| "GoogleAnalytics";
|
||||
|
||||
export type ConfigItem = {
|
||||
name: string;
|
||||
@@ -70,7 +54,7 @@ export type ConfigItem = {
|
||||
};
|
||||
|
||||
type ConfigType = {
|
||||
[key in SettingsPage]: ConfigItem;
|
||||
[key in string]: ConfigItem;
|
||||
};
|
||||
|
||||
const useSettingsConfig = () => {
|
||||
@@ -178,6 +162,16 @@ const useSettingsConfig = () => {
|
||||
icon: ExportIcon,
|
||||
},
|
||||
// Integrations
|
||||
...mapValues(loadPlugins(), (plugin) => {
|
||||
return {
|
||||
name: plugin.config.name,
|
||||
path: integrationSettingsPath(plugin.id),
|
||||
group: t("Integrations"),
|
||||
component: plugin.settings,
|
||||
enabled: !!plugin.settings && can.update,
|
||||
icon: plugin.icon,
|
||||
} as ConfigItem;
|
||||
}),
|
||||
Webhooks: {
|
||||
name: t("Webhooks"),
|
||||
path: "/settings/webhooks",
|
||||
@@ -188,23 +182,15 @@ const useSettingsConfig = () => {
|
||||
},
|
||||
SelfHosted: {
|
||||
name: t("Self Hosted"),
|
||||
path: "/settings/integrations/self-hosted",
|
||||
path: integrationSettingsPath("self-hosted"),
|
||||
component: SelfHosted,
|
||||
enabled: can.update,
|
||||
group: t("Integrations"),
|
||||
icon: BuildingBlocksIcon,
|
||||
},
|
||||
Slack: {
|
||||
name: "Slack",
|
||||
path: "/settings/integrations/slack",
|
||||
component: Slack,
|
||||
enabled: can.update && (!!env.SLACK_CLIENT_ID || isCloudHosted),
|
||||
group: t("Integrations"),
|
||||
icon: SlackIcon,
|
||||
},
|
||||
GoogleAnalytics: {
|
||||
name: t("Google Analytics"),
|
||||
path: "/settings/integrations/google-analytics",
|
||||
path: integrationSettingsPath("google-analytics"),
|
||||
component: GoogleAnalytics,
|
||||
enabled: can.update,
|
||||
group: t("Integrations"),
|
||||
@@ -212,7 +198,7 @@ const useSettingsConfig = () => {
|
||||
},
|
||||
Zapier: {
|
||||
name: "Zapier",
|
||||
path: "/settings/integrations/zapier",
|
||||
path: integrationSettingsPath("zapier"),
|
||||
component: Zapier,
|
||||
enabled: can.update && isCloudHosted,
|
||||
group: t("Integrations"),
|
||||
@@ -232,7 +218,7 @@ const useSettingsConfig = () => {
|
||||
const enabledConfigs = React.useMemo(
|
||||
() =>
|
||||
Object.keys(config).reduce(
|
||||
(acc, key: SettingsPage) =>
|
||||
(acc, key: string) =>
|
||||
config[key].enabled ? [...acc, config[key]] : acc,
|
||||
[]
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { Switch, Redirect } from "react-router-dom";
|
||||
import { Switch } from "react-router-dom";
|
||||
import Error404 from "~/scenes/Error404";
|
||||
import Route from "~/components/ProfiledRoute";
|
||||
import useSettingsConfig from "~/hooks/useSettingsConfig";
|
||||
@@ -17,10 +17,6 @@ export default function SettingsRoutes() {
|
||||
component={config.component}
|
||||
/>
|
||||
))}
|
||||
{/* old routes */}
|
||||
<Redirect from="/settings/import-export" to="/settings/export" />
|
||||
<Redirect from="/settings/people" to="/settings/members" />
|
||||
<Redirect from="/settings/profile" to="/settings" />
|
||||
<Route component={Error404} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
9
app/typings/window.d.ts
vendored
9
app/typings/window.d.ts
vendored
@@ -1,4 +1,13 @@
|
||||
declare global {
|
||||
interface NodeRequire {
|
||||
/** A special feature supported by webpack's compiler that allows you to get all matching modules starting from some base directory. */
|
||||
context: (
|
||||
directory: string,
|
||||
useSubdirectories: boolean,
|
||||
regExp: RegExp
|
||||
) => any;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
dataLayer: any[];
|
||||
gtag: (...args: any[]) => void;
|
||||
|
||||
37
app/utils/plugins.ts
Normal file
37
app/utils/plugins.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
interface Plugin {
|
||||
id: string;
|
||||
config: {
|
||||
name: string;
|
||||
description: string;
|
||||
requiredEnvVars?: string[];
|
||||
};
|
||||
settings: React.FC;
|
||||
icon: React.FC;
|
||||
}
|
||||
|
||||
export function loadPlugins(): { [id: string]: Plugin } {
|
||||
const plugins = {};
|
||||
|
||||
function importAll(r: any, property: string) {
|
||||
r.keys().forEach((key: string) => {
|
||||
const id = key.split("/")[1];
|
||||
plugins[id] = plugins[id] || {
|
||||
id,
|
||||
};
|
||||
|
||||
const plugin = r(key);
|
||||
plugins[id][property] = "default" in plugin ? plugin.default : plugin;
|
||||
});
|
||||
}
|
||||
importAll(
|
||||
require.context("../../plugins", true, /client\/Settings\.[tj]sx?$/),
|
||||
"settings"
|
||||
);
|
||||
importAll(
|
||||
require.context("../../plugins", true, /client\/Icon\.[tj]sx?$/),
|
||||
"icon"
|
||||
);
|
||||
importAll(require.context("../../plugins", true, /plugin\.json?$/), "config");
|
||||
|
||||
return plugins;
|
||||
}
|
||||
13
build.sh
13
build.sh
@@ -1,5 +1,16 @@
|
||||
#!/bin/sh
|
||||
yarn concurrently "yarn babel --extensions .ts,.tsx --quiet -d ./build/server ./server" "yarn babel --extensions .ts,.tsx --quiet -d ./build/shared ./shared"
|
||||
|
||||
# Compile server and shared
|
||||
yarn concurrently "yarn babel --extensions .ts,.tsx --quiet -d ./build/server ./server" \
|
||||
"yarn babel --extensions .ts,.tsx --quiet -d ./build/shared ./shared"
|
||||
|
||||
# Compile code in packages
|
||||
for d in ./plugins/*; do
|
||||
# Get the name of the folder
|
||||
package=$(basename "$d")
|
||||
yarn babel --extensions .ts,.tsx --quiet -d "./build/plugins/$package/server" "./plugins/$package/server"
|
||||
cp ./plugins/$package/plugin.json ./build/plugins/$package/plugin.json
|
||||
done
|
||||
|
||||
# Copy static files
|
||||
cp ./server/collaboration/Procfile ./build/server/collaboration/Procfile
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf build",
|
||||
"copy:i18n": "mkdir -p ./build/shared/i18n && cp -R ./shared/i18n/locales ./build/shared/i18n",
|
||||
"build:i18n": "i18next --silent 'shared/**/*.tsx' 'shared/**/*.ts' 'app/**/*.tsx' 'app/**/*.ts' 'server/**/*.ts' 'server/**/*.tsx' && yarn copy:i18n",
|
||||
"build:i18n": "i18next --silent '{shared,app,server,plugins}/**/*.{ts,tsx}' && yarn copy:i18n",
|
||||
"build:server": "./build.sh",
|
||||
"build:webpack": "webpack --config webpack.config.prod.js",
|
||||
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
|
||||
@@ -100,6 +100,7 @@
|
||||
"fs-extra": "^4.0.2",
|
||||
"fuzzy-search": "^3.2.1",
|
||||
"gemoji": "6.x",
|
||||
"glob": "^8.1.0",
|
||||
"http-errors": "2.0.0",
|
||||
"i18next": "^22.4.8",
|
||||
"i18next-fs-backend": "^2.1.1",
|
||||
@@ -240,6 +241,7 @@
|
||||
"@types/formidable": "^2.0.5",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/fuzzy-search": "^2.1.2",
|
||||
"@types/glob": "^8.0.1",
|
||||
"@types/google.analytics": "^0.0.42",
|
||||
"@types/inline-css": "^3.0.1",
|
||||
"@types/invariant": "^2.2.35",
|
||||
|
||||
@@ -7,10 +7,7 @@ type Props = {
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export default function SlackIcon({
|
||||
size = 24,
|
||||
color = "currentColor",
|
||||
}: Props) {
|
||||
export default function Icon({ size = 24, color = "currentColor" }: Props) {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
@@ -9,7 +9,6 @@ import Integration from "~/models/Integration";
|
||||
import Button from "~/components/Button";
|
||||
import Heading from "~/components/Heading";
|
||||
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
||||
import SlackIcon from "~/components/Icons/SlackIcon";
|
||||
import List from "~/components/List";
|
||||
import ListItem from "~/components/List/Item";
|
||||
import Notice from "~/components/Notice";
|
||||
@@ -19,6 +18,7 @@ import env from "~/env";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useQuery from "~/hooks/useQuery";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import SlackIcon from "./Icon";
|
||||
import SlackButton from "./components/SlackButton";
|
||||
import SlackListItem from "./components/SlackListItem";
|
||||
|
||||
5
plugins/slack/plugin.json
Normal file
5
plugins/slack/plugin.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Slack",
|
||||
"description": "Adds a Slack authentication provider, support for the /outline slash command, and link unfurling.",
|
||||
"requiredEnvVars": ["SLACK_CLIENT_ID", "SLACK_CLIENT_SECRET"]
|
||||
}
|
||||
3
plugins/slack/server/.babelrc
Normal file
3
plugins/slack/server/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../../server/.babelrc"
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import env from "@server/env";
|
||||
import { IntegrationAuthentication, SearchQuery } from "@server/models";
|
||||
import { buildDocument, buildIntegration } from "@server/test/factories";
|
||||
import { seed, getTestServer } from "@server/test/support";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import * as Slack from "../slack";
|
||||
|
||||
jest.mock("@server/utils/slack", () => ({
|
||||
post: jest.fn(),
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
IntegrationAuthentication,
|
||||
} from "@server/models";
|
||||
import SearchHelper from "@server/models/helpers/SearchHelper";
|
||||
import { presentSlackAttachment } from "@server/presenters";
|
||||
import { APIContext } from "@server/types";
|
||||
import { opts } from "@server/utils/i18n";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import { assertPresent } from "@server/validation";
|
||||
import presentMessageAttachment from "../presenters/messageAttachment";
|
||||
import * as Slack from "../slack";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@@ -140,7 +140,7 @@ router.post("hooks.interactive", async (ctx: APIContext) => {
|
||||
response_type: "in_channel",
|
||||
replace_original: false,
|
||||
attachments: [
|
||||
presentSlackAttachment(
|
||||
presentMessageAttachment(
|
||||
document,
|
||||
team,
|
||||
document.collection,
|
||||
@@ -329,7 +329,7 @@ router.post("hooks.slack", async (ctx: APIContext) => {
|
||||
.toLowerCase()
|
||||
.match(escapeRegExp(text.toLowerCase()));
|
||||
attachments.push(
|
||||
presentSlackAttachment(
|
||||
presentMessageAttachment(
|
||||
result.document,
|
||||
team,
|
||||
result.document.collection,
|
||||
@@ -4,6 +4,7 @@ import Router from "koa-router";
|
||||
import { Profile } from "passport";
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
@@ -21,8 +22,8 @@ import {
|
||||
getTeamFromContext,
|
||||
StateStore,
|
||||
} from "@server/utils/passport";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import { assertPresent, assertUuid } from "@server/validation";
|
||||
import * as Slack from "../slack";
|
||||
|
||||
type SlackProfile = Profile & {
|
||||
team: {
|
||||
@@ -59,11 +60,6 @@ function redirectOnClient(ctx: Context, url: string) {
|
||||
</head>`;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "Slack",
|
||||
enabled: !!env.SLACK_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
const strategy = new SlackStrategy(
|
||||
{
|
||||
@@ -142,7 +138,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
assertPresent(code || error, "code is required");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
ctx.redirect(integrationSettingsPath(`slack?error=${error}`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,12 +157,12 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
integrationSettingsPath(`slack?error=unauthenticated`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
integrationSettingsPath(`slack?error=unauthenticated`)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -190,7 +186,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
serviceTeamId: data.team_id,
|
||||
},
|
||||
});
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
ctx.redirect(integrationSettingsPath("slack"));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -208,7 +204,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
assertUuid(collectionId, "collectionId must be an uuid");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
ctx.redirect(integrationSettingsPath(`slack?error=${error}`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -232,7 +228,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
integrationSettingsPath(`slack?error=unauthenticated`)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -261,7 +257,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
channelId: data.incoming_webhook.channel_id,
|
||||
},
|
||||
});
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
ctx.redirect(integrationSettingsPath("slack"));
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ type Action = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
function presentSlackAttachment(
|
||||
function presentMessageAttachment(
|
||||
document: Document,
|
||||
team: Team,
|
||||
collection?: Collection | null,
|
||||
@@ -35,4 +35,4 @@ function presentSlackAttachment(
|
||||
|
||||
export default traceFunction({
|
||||
spanName: "presenters",
|
||||
})(presentSlackAttachment);
|
||||
})(presentMessageAttachment);
|
||||
@@ -3,14 +3,14 @@ import { Op } from "sequelize";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import env from "@server/env";
|
||||
import { Document, Integration, Collection, Team } from "@server/models";
|
||||
import { presentSlackAttachment } from "@server/presenters";
|
||||
import BaseProcessor from "@server/queues/processors/BaseProcessor";
|
||||
import {
|
||||
DocumentEvent,
|
||||
IntegrationEvent,
|
||||
RevisionEvent,
|
||||
Event,
|
||||
} from "@server/types";
|
||||
import BaseProcessor from "./BaseProcessor";
|
||||
import presentMessageAttachment from "../presenters/messageAttachment";
|
||||
|
||||
export default class SlackProcessor extends BaseProcessor {
|
||||
static applicableEvents: Event["name"][] = [
|
||||
@@ -131,7 +131,7 @@ export default class SlackProcessor extends BaseProcessor {
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
attachments: [
|
||||
presentSlackAttachment(document, team, document.collection),
|
||||
presentMessageAttachment(document, team, document.collection),
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import querystring from "querystring";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import env from "@server/env";
|
||||
import { InvalidRequestError } from "../errors";
|
||||
import { InvalidRequestError } from "@server/errors";
|
||||
|
||||
const SLACK_API_URL = "https://slack.com/api";
|
||||
|
||||
@@ -18,7 +18,6 @@ import presentProviderConfig from "./providerConfig";
|
||||
import presentRevision from "./revision";
|
||||
import presentSearchQuery from "./searchQuery";
|
||||
import presentShare from "./share";
|
||||
import presentSlackAttachment from "./slackAttachment";
|
||||
import presentStar from "./star";
|
||||
import presentSubscription from "./subscription";
|
||||
import presentTeam from "./team";
|
||||
@@ -48,7 +47,6 @@ export {
|
||||
presentRevision,
|
||||
presentSearchQuery,
|
||||
presentShare,
|
||||
presentSlackAttachment,
|
||||
presentStar,
|
||||
presentSubscription,
|
||||
presentTeam,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { signin } from "@shared/utils/urlHelpers";
|
||||
import { signin } from "@shared/utils/routeHelpers";
|
||||
import { AuthenticationProviderConfig } from "@server/routes/auth/providers";
|
||||
|
||||
export default function presentProviderConfig(
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import path from "path";
|
||||
import { glob } from "glob";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { requireDirectory } from "@server/utils/fs";
|
||||
import BaseProcessor from "./BaseProcessor";
|
||||
|
||||
const processors = {};
|
||||
|
||||
requireDirectory(__dirname).forEach(([module, id]) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'default' does not exist on type 'unknown'
|
||||
const { default: Processor } = module;
|
||||
|
||||
if (id === "index") {
|
||||
return;
|
||||
requireDirectory<{ default: BaseProcessor }>(__dirname).forEach(
|
||||
([module, id]) => {
|
||||
if (id === "index") {
|
||||
return;
|
||||
}
|
||||
processors[id] = module.default;
|
||||
}
|
||||
);
|
||||
|
||||
processors[id] = Processor;
|
||||
});
|
||||
glob
|
||||
.sync("build/plugins/*/server/processors/!(*.test).js")
|
||||
.forEach((filePath: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const processor = require(path.join(process.cwd(), filePath)).default;
|
||||
const name = path.basename(filePath, ".js");
|
||||
processors[name] = processor;
|
||||
Logger.debug("processor", `Registered processor ${name}`);
|
||||
});
|
||||
|
||||
export default processors;
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import path from "path";
|
||||
import { glob } from "glob";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { requireDirectory } from "@server/utils/fs";
|
||||
import BaseTask from "./BaseTask";
|
||||
|
||||
const tasks = {};
|
||||
|
||||
requireDirectory(__dirname).forEach(([module, id]) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'default' does not exist on type 'unknown'
|
||||
const { default: Task } = module;
|
||||
|
||||
if (id === "index") {
|
||||
return;
|
||||
requireDirectory<{ default: BaseTask<any> }>(__dirname).forEach(
|
||||
([module, id]) => {
|
||||
if (id === "index") {
|
||||
return;
|
||||
}
|
||||
tasks[id] = module.default;
|
||||
}
|
||||
);
|
||||
|
||||
tasks[id] = Task;
|
||||
});
|
||||
glob
|
||||
.sync("build/plugins/*/server/tasks/!(*.test).js")
|
||||
.forEach((filePath: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const task = require(path.join(process.cwd(), filePath)).default;
|
||||
const name = path.basename(filePath, ".js");
|
||||
tasks[name] = task;
|
||||
Logger.debug("task", `Registered task ${name}`);
|
||||
});
|
||||
|
||||
export default tasks;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import path from "path";
|
||||
import glob from "glob";
|
||||
import Koa, { BaseContext } from "koa";
|
||||
import bodyParser from "koa-body";
|
||||
import Router from "koa-router";
|
||||
import userAgent, { UserAgentContext } from "koa-useragent";
|
||||
import env from "@server/env";
|
||||
import { NotFoundError } from "@server/errors";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { AppState, AppContext } from "@server/types";
|
||||
import apiKeys from "./apiKeys";
|
||||
import attachments from "./attachments";
|
||||
@@ -16,7 +19,6 @@ import documents from "./documents";
|
||||
import events from "./events";
|
||||
import fileOperationsRoute from "./fileOperations";
|
||||
import groups from "./groups";
|
||||
import hooks from "./hooks";
|
||||
import integrations from "./integrations";
|
||||
import apiWrapper from "./middlewares/apiWrapper";
|
||||
import editor from "./middlewares/editor";
|
||||
@@ -48,6 +50,16 @@ api.use<BaseContext, UserAgentContext>(userAgent);
|
||||
api.use(apiWrapper());
|
||||
api.use(editor());
|
||||
|
||||
// register package API routes before others to allow for overrides
|
||||
glob
|
||||
.sync("build/plugins/*/server/api/!(*.test).js")
|
||||
.forEach((filePath: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const pkg: Router = require(path.join(process.cwd(), filePath)).default;
|
||||
router.use("/", pkg.routes());
|
||||
Logger.debug("lifecycle", `Registered API routes for ${filePath}`);
|
||||
});
|
||||
|
||||
// routes
|
||||
router.use("/", auth.routes());
|
||||
router.use("/", authenticationProviders.routes());
|
||||
@@ -58,7 +70,6 @@ router.use("/", documents.routes());
|
||||
router.use("/", pins.routes());
|
||||
router.use("/", revisions.routes());
|
||||
router.use("/", views.routes());
|
||||
router.use("/", hooks.routes());
|
||||
router.use("/", apiKeys.routes());
|
||||
router.use("/", searches.routes());
|
||||
router.use("/", shares.routes());
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import path from "path";
|
||||
import { glob } from "glob";
|
||||
import Router from "koa-router";
|
||||
import { sortBy } from "lodash";
|
||||
import env from "@server/env";
|
||||
import { requireDirectory } from "@server/utils/fs";
|
||||
|
||||
export type AuthenticationProviderConfig = {
|
||||
@@ -11,8 +15,10 @@ export type AuthenticationProviderConfig = {
|
||||
|
||||
const authenticationProviderConfigs: AuthenticationProviderConfig[] = [];
|
||||
|
||||
requireDirectory(__dirname).forEach(([module, id]) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'config' does not exist on type 'unknown'... Remove this comment to see the full error message
|
||||
requireDirectory<{
|
||||
default: Router;
|
||||
config: { name: string; enabled: boolean };
|
||||
}>(__dirname).forEach(([module, id]) => {
|
||||
const { config, default: router } = module;
|
||||
|
||||
if (id === "index") {
|
||||
@@ -41,4 +47,34 @@ requireDirectory(__dirname).forEach(([module, id]) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Temporarily also include plugins here until all auth methods are moved over.
|
||||
glob
|
||||
.sync(
|
||||
(env.ENVIRONMENT === "test" ? "" : "build/") +
|
||||
"plugins/*/server/auth/!(*.test).[jt]s"
|
||||
)
|
||||
.forEach((filePath: string) => {
|
||||
const authProvider = require(path.join(process.cwd(), filePath)).default;
|
||||
const id = filePath.replace("build/", "").split("/")[1];
|
||||
const config = require(path.join(
|
||||
process.cwd(),
|
||||
env.ENVIRONMENT === "test" ? "" : "build",
|
||||
"plugins",
|
||||
id,
|
||||
"plugin.json"
|
||||
));
|
||||
|
||||
// Test the all required env vars are set for the auth provider
|
||||
const enabled = (config.requiredEnvVars ?? []).every(
|
||||
(name: string) => !!env[name]
|
||||
);
|
||||
|
||||
authenticationProviderConfigs.push({
|
||||
id,
|
||||
name: config.name,
|
||||
enabled,
|
||||
router: authProvider,
|
||||
});
|
||||
});
|
||||
|
||||
export default sortBy(authenticationProviderConfigs, "id");
|
||||
|
||||
@@ -9,7 +9,7 @@ export function deserializeFilename(text: string): string {
|
||||
return text.replace(/%2F/g, "/").replace(/%5C/g, "\\");
|
||||
}
|
||||
|
||||
export function requireDirectory<T>(dirName: string): [T, string][] {
|
||||
export function getFilenamesInDirectory(dirName: string): string[] {
|
||||
return fs
|
||||
.readdirSync(dirName)
|
||||
.filter(
|
||||
@@ -18,10 +18,13 @@ export function requireDirectory<T>(dirName: string): [T, string][] {
|
||||
file.match(/\.[jt]s$/) &&
|
||||
file !== path.basename(__filename) &&
|
||||
!file.includes(".test")
|
||||
)
|
||||
.map((fileName) => {
|
||||
const filePath = path.join(dirName, fileName);
|
||||
const name = path.basename(filePath.replace(/\.[jt]s$/, ""));
|
||||
return [require(filePath), name];
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
export function requireDirectory<T>(dirName: string): [T, string][] {
|
||||
return getFilenamesInDirectory(dirName).map((fileName) => {
|
||||
const filePath = path.join(dirName, fileName);
|
||||
const name = path.basename(filePath.replace(/\.[jt]s$/, ""));
|
||||
return [require(filePath), name];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,8 +312,8 @@
|
||||
"Groups": "Groups",
|
||||
"Shared Links": "Shared Links",
|
||||
"Import": "Import",
|
||||
"Webhooks": "Webhooks",
|
||||
"Integrations": "Integrations",
|
||||
"Webhooks": "Webhooks",
|
||||
"Self Hosted": "Self Hosted",
|
||||
"Google Analytics": "Google Analytics",
|
||||
"Show path to document": "Show path to document",
|
||||
@@ -662,14 +662,6 @@
|
||||
"Last accessed": "Last accessed",
|
||||
"Date shared": "Date shared",
|
||||
"Shared nested": "Shared nested",
|
||||
"Add to Slack": "Add to Slack",
|
||||
"Settings saved": "Settings saved",
|
||||
"document published": "document published",
|
||||
"document updated": "document updated",
|
||||
"Posting to the <em>{{ channelName }}</em> channel on": "Posting to the <em>{{ channelName }}</em> channel on",
|
||||
"These events should be posted to Slack": "These events should be posted to Slack",
|
||||
"Document updated": "Document updated",
|
||||
"Disconnect": "Disconnect",
|
||||
"API token copied to clipboard": "API token copied to clipboard",
|
||||
"Revoke token": "Revoke token",
|
||||
"Copied": "Copied",
|
||||
@@ -695,6 +687,7 @@
|
||||
"Subscribed events": "Subscribed events",
|
||||
"Edit webhook": "Edit webhook",
|
||||
"Webhook created": "Webhook created",
|
||||
"Settings saved": "Settings saved",
|
||||
"Logo updated": "Logo updated",
|
||||
"Unable to upload new logo": "Unable to upload new logo",
|
||||
"These settings affect the way that your knowledge base appears to everyone on the team.": "These settings affect the way that your knowledge base appears to everyone on the team.",
|
||||
@@ -732,6 +725,7 @@
|
||||
"Everyone that has signed into {{appName}} is listed here. It’s possible that there are other users who have access through {team.signinMethods} but haven’t signed in yet.": "Everyone that has signed into {{appName}} is listed here. It’s possible that there are other users who have access through {team.signinMethods} but haven’t signed in yet.",
|
||||
"Filter": "Filter",
|
||||
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
|
||||
"Document updated": "Document updated",
|
||||
"Receive a notification when a document you created is edited": "Receive a notification when a document you created is edited",
|
||||
"Collection created": "Collection created",
|
||||
"Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created",
|
||||
@@ -795,12 +789,6 @@
|
||||
"Sharing is currently disabled.": "Sharing is currently disabled.",
|
||||
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
|
||||
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.",
|
||||
"Whoops, you need to accept the permissions in Slack to connect{{appName}} to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect{{appName}} to your team. Try again?",
|
||||
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
|
||||
"Get rich previews of {{ appName }} links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of {{ appName }} links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
|
||||
"Connect {{appName}} collections to Slack channels. Messages will be automatically posted to Slack when documents are published or updated.": "Connect {{appName}} collections to Slack channels. Messages will be automatically posted to Slack when documents are published or updated.",
|
||||
"Connect": "Connect",
|
||||
"The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
|
||||
"New token": "New token",
|
||||
"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",
|
||||
@@ -832,6 +820,18 @@
|
||||
"This month": "This month",
|
||||
"Last month": "Last month",
|
||||
"This year": "This year",
|
||||
"Add to Slack": "Add to Slack",
|
||||
"document published": "document published",
|
||||
"document updated": "document updated",
|
||||
"Posting to the <em>{{ channelName }}</em> channel on": "Posting to the <em>{{ channelName }}</em> channel on",
|
||||
"These events should be posted to Slack": "These events should be posted to Slack",
|
||||
"Disconnect": "Disconnect",
|
||||
"Whoops, you need to accept the permissions in Slack to connect{{appName}} to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect{{appName}} to your team. Try again?",
|
||||
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
|
||||
"Get rich previews of {{ appName }} links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of {{ appName }} links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
|
||||
"Connect {{appName}} collections to Slack channels. Messages will be automatically posted to Slack when documents are published or updated.": "Connect {{appName}} collections to Slack channels. Messages will be automatically posted to Slack when documents are published or updated.",
|
||||
"Connect": "Connect",
|
||||
"The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
|
||||
"To search your knowledgebase use {{ command }}. \nYou’ve already learned how to get help with {{ command2 }}.": "To search your knowledgebase use {{ command }}. \nYou’ve already learned how to get help with {{ command2 }}.",
|
||||
"Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.": "Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.",
|
||||
"It looks like you haven’t signed in to {{ appName }} yet, so results may be limited": "It looks like you haven’t signed in to {{ appName }} yet, so results may be limited",
|
||||
|
||||
7
shared/utils/routeHelpers.ts
Normal file
7
shared/utils/routeHelpers.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function signin(service = "slack"): string {
|
||||
return `/auth/${service}`;
|
||||
}
|
||||
|
||||
export function integrationSettingsPath(id: string): string {
|
||||
return `/settings/integrations/${id}`;
|
||||
}
|
||||
@@ -48,10 +48,6 @@ export function changelogUrl(): string {
|
||||
return "https://www.getoutline.com/changelog";
|
||||
}
|
||||
|
||||
export function signin(service = "slack"): string {
|
||||
return `/auth/${service}`;
|
||||
}
|
||||
|
||||
export const SLUG_URL_REGEX = /^(?:[0-9a-zA-Z-_~]*-)?([a-zA-Z0-9]{10,15})$/;
|
||||
|
||||
export const SHARE_URL_SLUG_REGEX = /^[0-9a-z-]+$/;
|
||||
|
||||
@@ -25,6 +25,7 @@ module.exports = {
|
||||
include: [
|
||||
path.join(__dirname, 'app'),
|
||||
path.join(__dirname, 'shared'),
|
||||
path.join(__dirname, 'plugins'),
|
||||
],
|
||||
options: {
|
||||
cacheDirectory: true
|
||||
|
||||
38
yarn.lock
38
yarn.lock
@@ -2985,6 +2985,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/fuzzy-search/-/fuzzy-search-2.1.2.tgz#d57b2af8fb723baa1792d40d511f431c4c8f75af"
|
||||
integrity sha512-YOqA50Z3xcycm4Br5+MBUpSumfdOAcv34A8A8yFn62zBQPTzJSXQk11qYE5w8BWQ0KrVThXUgEQh7ZLrYI1NaQ==
|
||||
|
||||
"@types/glob@^8.0.1":
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.0.1.tgz#6e3041640148b7764adf21ce5c7138ad454725b0"
|
||||
integrity sha512-8bVUjXZvJacUFkJXHdyZ9iH1Eaj5V7I8c4NdH5sQJsdXkqT4CA5Dhb4yb4VE/3asyx4L9ayZr1NIhTsWHczmMw==
|
||||
dependencies:
|
||||
"@types/minimatch" "^5.1.2"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/google.analytics@^0.0.42":
|
||||
version "0.0.42"
|
||||
resolved "https://registry.yarnpkg.com/@types/google.analytics/-/google.analytics-0.0.42.tgz#efe6ef9251a22ec8208dbb09f221a48a1863d720"
|
||||
@@ -3250,6 +3258,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
|
||||
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
|
||||
|
||||
"@types/minimatch@^5.1.2":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
|
||||
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
|
||||
|
||||
"@types/ms@*":
|
||||
version "0.7.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
||||
@@ -4868,6 +4881,13 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
|
||||
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^2.3.1, braces@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
|
||||
@@ -8374,6 +8394,17 @@ glob@7.2.0, glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
|
||||
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^5.0.1"
|
||||
once "^1.3.0"
|
||||
|
||||
global@~4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
|
||||
@@ -11209,6 +11240,13 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^5.0.1:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
|
||||
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||
|
||||
Reference in New Issue
Block a user