Refactor and regroup urlHelpers utils (#6462)
* fix: refactor urlHelpers * fix: move to /plugins/slack/shared * fix: remove .babelrc * fix: remove Outline class * fix: Slack -> SlackUtils * fix: UrlHelper class
This commit is contained in:
@@ -14,13 +14,8 @@ import {
|
|||||||
ShapesIcon,
|
ShapesIcon,
|
||||||
} from "outline-icons";
|
} from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import { isMac } from "@shared/utils/browser";
|
import { isMac } from "@shared/utils/browser";
|
||||||
import {
|
|
||||||
developersUrl,
|
|
||||||
changelogUrl,
|
|
||||||
feedbackUrl,
|
|
||||||
githubIssuesUrl,
|
|
||||||
} from "@shared/utils/urlHelpers";
|
|
||||||
import stores from "~/stores";
|
import stores from "~/stores";
|
||||||
import SearchQuery from "~/models/SearchQuery";
|
import SearchQuery from "~/models/SearchQuery";
|
||||||
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
|
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
|
||||||
@@ -139,7 +134,7 @@ export const openAPIDocumentation = createAction({
|
|||||||
section: NavigationSection,
|
section: NavigationSection,
|
||||||
iconInContextMenu: false,
|
iconInContextMenu: false,
|
||||||
icon: <OpenIcon />,
|
icon: <OpenIcon />,
|
||||||
perform: () => window.open(developersUrl()),
|
perform: () => window.open(UrlHelper.developers),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const toggleSidebar = createAction({
|
export const toggleSidebar = createAction({
|
||||||
@@ -156,14 +151,14 @@ export const openFeedbackUrl = createAction({
|
|||||||
section: NavigationSection,
|
section: NavigationSection,
|
||||||
iconInContextMenu: false,
|
iconInContextMenu: false,
|
||||||
icon: <EmailIcon />,
|
icon: <EmailIcon />,
|
||||||
perform: () => window.open(feedbackUrl()),
|
perform: () => window.open(UrlHelper.contact),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const openBugReportUrl = createAction({
|
export const openBugReportUrl = createAction({
|
||||||
name: ({ t }) => t("Report a bug"),
|
name: ({ t }) => t("Report a bug"),
|
||||||
analyticsName: "Open bug report",
|
analyticsName: "Open bug report",
|
||||||
section: NavigationSection,
|
section: NavigationSection,
|
||||||
perform: () => window.open(githubIssuesUrl()),
|
perform: () => window.open(UrlHelper.github),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const openChangelog = createAction({
|
export const openChangelog = createAction({
|
||||||
@@ -172,7 +167,7 @@ export const openChangelog = createAction({
|
|||||||
section: NavigationSection,
|
section: NavigationSection,
|
||||||
iconInContextMenu: false,
|
iconInContextMenu: false,
|
||||||
icon: <OpenIcon />,
|
icon: <OpenIcon />,
|
||||||
perform: () => window.open(changelogUrl()),
|
perform: () => window.open(UrlHelper.changelog),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const openKeyboardShortcuts = createAction({
|
export const openKeyboardShortcuts = createAction({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
|||||||
import { withTranslation, Trans, WithTranslation } from "react-i18next";
|
import { withTranslation, Trans, WithTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { s } from "@shared/styles";
|
import { s } from "@shared/styles";
|
||||||
import { githubIssuesUrl, feedbackUrl } from "@shared/utils/urlHelpers";
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import CenteredContent from "~/components/CenteredContent";
|
import CenteredContent from "~/components/CenteredContent";
|
||||||
import PageTitle from "~/components/PageTitle";
|
import PageTitle from "~/components/PageTitle";
|
||||||
@@ -57,7 +57,7 @@ class ErrorBoundary extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleReportBug = () => {
|
handleReportBug = () => {
|
||||||
window.open(isCloudHosted ? feedbackUrl() : githubIssuesUrl());
|
window.open(isCloudHosted ? UrlHelper.contact : UrlHelper.github);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import styled, { useTheme } from "styled-components";
|
import styled, { useTheme } from "styled-components";
|
||||||
import { s } from "@shared/styles";
|
import { s } from "@shared/styles";
|
||||||
import { SHARE_URL_SLUG_REGEX } from "@shared/utils/urlHelpers";
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import Share from "~/models/Share";
|
import Share from "~/models/Share";
|
||||||
import Input, { NativeInput } from "~/components/Input";
|
import Input, { NativeInput } from "~/components/Input";
|
||||||
@@ -78,7 +78,7 @@ function PublicAccess({ document, share, sharedParent }: Props) {
|
|||||||
|
|
||||||
const val = ev.target.value;
|
const val = ev.target.value;
|
||||||
setUrlId(val);
|
setUrlId(val);
|
||||||
if (val && !SHARE_URL_SLUG_REGEX.test(val)) {
|
if (val && !UrlHelper.SHARE_URL_SLUG_REGEX.test(val)) {
|
||||||
setValidationError(
|
setValidationError(
|
||||||
t("Only lowercase letters, digits and dashes allowed")
|
t("Only lowercase letters, digits and dashes allowed")
|
||||||
);
|
);
|
||||||
|
|||||||
8
build.js
8
build.js
@@ -53,6 +53,14 @@ async function build() {
|
|||||||
`yarn babel --extensions .ts,.tsx --quiet -d "./build/plugins/${plugin}/server" "./plugins/${plugin}/server"`
|
`yarn babel --extensions .ts,.tsx --quiet -d "./build/plugins/${plugin}/server" "./plugins/${plugin}/server"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasShared = existsSync(`./plugins/${plugin}/shared`);
|
||||||
|
|
||||||
|
if (hasShared) {
|
||||||
|
await execAsync(
|
||||||
|
`yarn babel --extensions .ts,.tsx --quiet -d "./build/plugins/${plugin}/shared" "./plugins/${plugin}/shared"`
|
||||||
|
);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import env from "~/env";
|
|||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import useQuery from "~/hooks/useQuery";
|
import useQuery from "~/hooks/useQuery";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
|
import { SlackUtils } from "../shared/SlackUtils";
|
||||||
import SlackIcon from "./Icon";
|
import SlackIcon from "./Icon";
|
||||||
import SlackButton from "./components/SlackButton";
|
import SlackButton from "./components/SlackButton";
|
||||||
import SlackListItem from "./components/SlackListItem";
|
import SlackListItem from "./components/SlackListItem";
|
||||||
@@ -104,7 +105,7 @@ function Slack() {
|
|||||||
// "users:read",
|
// "users:read",
|
||||||
// "users:read.email",
|
// "users:read.email",
|
||||||
]}
|
]}
|
||||||
redirectUri={`${env.URL}/auth/slack.commands`}
|
redirectUri={SlackUtils.commandsUrl()}
|
||||||
state={team.id}
|
state={team.id}
|
||||||
icon={<SlackIcon />}
|
icon={<SlackIcon />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { slackAuth } from "@shared/utils/urlHelpers";
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import env from "~/env";
|
import { SlackUtils } from "../../shared/SlackUtils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
@@ -16,16 +15,7 @@ function SlackButton({ state = "", scopes, redirectUri, label, icon }: Props) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!env.SLACK_CLIENT_ID) {
|
window.location.href = SlackUtils.authUrl(state, scopes, redirectUri);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = slackAuth(
|
|
||||||
state,
|
|
||||||
scopes,
|
|
||||||
env.SLACK_CLIENT_ID,
|
|
||||||
redirectUri
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Router from "koa-router";
|
|||||||
import { Profile } from "passport";
|
import { Profile } from "passport";
|
||||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||||
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
|
||||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||||
import auth from "@server/middlewares/authentication";
|
import auth from "@server/middlewares/authentication";
|
||||||
import passportMiddleware from "@server/middlewares/passport";
|
import passportMiddleware from "@server/middlewares/passport";
|
||||||
@@ -25,6 +24,7 @@ import {
|
|||||||
import env from "../env";
|
import env from "../env";
|
||||||
import * as Slack from "../slack";
|
import * as Slack from "../slack";
|
||||||
import * as T from "./schema";
|
import * as T from "./schema";
|
||||||
|
import { SlackUtils } from "plugins/slack/shared/SlackUtils";
|
||||||
|
|
||||||
type SlackProfile = Profile & {
|
type SlackProfile = Profile & {
|
||||||
team: {
|
team: {
|
||||||
@@ -66,7 +66,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
{
|
{
|
||||||
clientID: env.SLACK_CLIENT_ID,
|
clientID: env.SLACK_CLIENT_ID,
|
||||||
clientSecret: env.SLACK_CLIENT_SECRET,
|
clientSecret: env.SLACK_CLIENT_SECRET,
|
||||||
callbackURL: `${env.URL}/auth/slack.callback`,
|
callbackURL: SlackUtils.callbackUrl(),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
// @ts-expect-error StateStore
|
// @ts-expect-error StateStore
|
||||||
store: new StateStore(),
|
store: new StateStore(),
|
||||||
@@ -139,7 +139,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
const { user } = ctx.state.auth;
|
const { user } = ctx.state.auth;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
ctx.redirect(integrationSettingsPath(`slack?error=${error}`));
|
ctx.redirect(SlackUtils.errorUrl(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,23 +154,21 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
});
|
});
|
||||||
return redirectOnClient(
|
return redirectOnClient(
|
||||||
ctx,
|
ctx,
|
||||||
`${team.url}/auth/slack.commands?${ctx.request.querystring}`
|
SlackUtils.commandsUrl({
|
||||||
|
baseUrl: team.url,
|
||||||
|
params: ctx.request.querystring,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return ctx.redirect(
|
return ctx.redirect(SlackUtils.errorUrl("unauthenticated"));
|
||||||
integrationSettingsPath(`slack?error=unauthenticated`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ctx.redirect(
|
return ctx.redirect(SlackUtils.errorUrl("unauthenticated"));
|
||||||
integrationSettingsPath(`slack?error=unauthenticated`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = `${env.URL}/auth/slack.commands`;
|
|
||||||
// validation middleware ensures that code is non-null at this point
|
// validation middleware ensures that code is non-null at this point
|
||||||
const data = await Slack.oauthAccess(code!, endpoint);
|
const data = await Slack.oauthAccess(code!, SlackUtils.commandsUrl());
|
||||||
const authentication = await IntegrationAuthentication.create({
|
const authentication = await IntegrationAuthentication.create({
|
||||||
service: IntegrationService.Slack,
|
service: IntegrationService.Slack,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -188,7 +186,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
serviceTeamId: data.team_id,
|
serviceTeamId: data.team_id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
ctx.redirect(integrationSettingsPath("slack"));
|
ctx.redirect(SlackUtils.url);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -203,7 +201,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
const { user } = ctx.state.auth;
|
const { user } = ctx.state.auth;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
ctx.redirect(integrationSettingsPath(`slack?error=${error}`));
|
ctx.redirect(SlackUtils.errorUrl(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,23 +219,21 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
});
|
});
|
||||||
return redirectOnClient(
|
return redirectOnClient(
|
||||||
ctx,
|
ctx,
|
||||||
`${team.url}/auth/slack.post?${ctx.request.querystring}`
|
SlackUtils.postUrl({
|
||||||
|
baseUrl: team.url,
|
||||||
|
params: ctx.request.querystring,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return ctx.redirect(
|
return ctx.redirect(SlackUtils.errorUrl("unauthenticated"));
|
||||||
integrationSettingsPath(`slack?error=unauthenticated`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ctx.redirect(
|
return ctx.redirect(SlackUtils.errorUrl("unauthenticated"));
|
||||||
integrationSettingsPath(`slack?error=unauthenticated`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = `${env.URL}/auth/slack.post`;
|
|
||||||
// validation middleware ensures that code is non-null at this point
|
// validation middleware ensures that code is non-null at this point
|
||||||
const data = await Slack.oauthAccess(code!, endpoint);
|
const data = await Slack.oauthAccess(code!, SlackUtils.postUrl());
|
||||||
const authentication = await IntegrationAuthentication.create({
|
const authentication = await IntegrationAuthentication.create({
|
||||||
service: IntegrationService.Slack,
|
service: IntegrationService.Slack,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -260,7 +256,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
channelId: data.incoming_webhook.channel_id,
|
channelId: data.incoming_webhook.channel_id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
ctx.redirect(integrationSettingsPath("slack"));
|
ctx.redirect(SlackUtils.url);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import querystring from "querystring";
|
import querystring from "querystring";
|
||||||
import { InvalidRequestError } from "@server/errors";
|
import { InvalidRequestError } from "@server/errors";
|
||||||
import fetch from "@server/utils/fetch";
|
import fetch from "@server/utils/fetch";
|
||||||
|
import { SlackUtils } from "../shared/SlackUtils";
|
||||||
import env from "./env";
|
import env from "./env";
|
||||||
|
|
||||||
const SLACK_API_URL = "https://slack.com/api";
|
const SLACK_API_URL = "https://slack.com/api";
|
||||||
@@ -49,7 +50,7 @@ export async function request(endpoint: string, body: Record<string, any>) {
|
|||||||
|
|
||||||
export async function oauthAccess(
|
export async function oauthAccess(
|
||||||
code: string,
|
code: string,
|
||||||
redirect_uri = `${env.URL}/auth/slack.callback`
|
redirect_uri = SlackUtils.callbackUrl()
|
||||||
) {
|
) {
|
||||||
return request("oauth.access", {
|
return request("oauth.access", {
|
||||||
client_id: env.SLACK_CLIENT_ID,
|
client_id: env.SLACK_CLIENT_ID,
|
||||||
|
|||||||
70
plugins/slack/shared/SlackUtils.ts
Normal file
70
plugins/slack/shared/SlackUtils.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import env from "@shared/env";
|
||||||
|
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
||||||
|
|
||||||
|
export class SlackUtils {
|
||||||
|
private static authBaseUrl = "https://slack.com/oauth/authorize";
|
||||||
|
|
||||||
|
static commandsUrl(
|
||||||
|
{ baseUrl, params }: { baseUrl: string; params?: string } = {
|
||||||
|
baseUrl: `${env.URL}`,
|
||||||
|
params: undefined,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return params
|
||||||
|
? `${baseUrl}/auth/slack.commands?${params}`
|
||||||
|
: `${baseUrl}/auth/slack.commands`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static callbackUrl(
|
||||||
|
{ baseUrl, params }: { baseUrl: string; params?: string } = {
|
||||||
|
baseUrl: `${env.URL}`,
|
||||||
|
params: undefined,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return params
|
||||||
|
? `${baseUrl}/auth/slack.callback?${params}`
|
||||||
|
: `${baseUrl}/auth/slack.callback`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static postUrl(
|
||||||
|
{ baseUrl, params }: { baseUrl: string; params?: string } = {
|
||||||
|
baseUrl: `${env.URL}`,
|
||||||
|
params: undefined,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return params
|
||||||
|
? `${baseUrl}/auth/slack.post?${params}`
|
||||||
|
: `${baseUrl}/auth/slack.post`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static errorUrl(err: string) {
|
||||||
|
return integrationSettingsPath(`slack?error=${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get url() {
|
||||||
|
return integrationSettingsPath("slack");
|
||||||
|
}
|
||||||
|
|
||||||
|
static authUrl(
|
||||||
|
state: string,
|
||||||
|
scopes: string[] = [
|
||||||
|
"identity.email",
|
||||||
|
"identity.basic",
|
||||||
|
"identity.avatar",
|
||||||
|
"identity.team",
|
||||||
|
],
|
||||||
|
redirectUri = SlackUtils.callbackUrl()
|
||||||
|
): string {
|
||||||
|
const baseUrl = SlackUtils.authBaseUrl;
|
||||||
|
const params = {
|
||||||
|
client_id: env.SLACK_CLIENT_ID,
|
||||||
|
scope: scopes ? scopes.join(" ") : "",
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
const urlParams = Object.keys(params)
|
||||||
|
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
||||||
|
.join("&");
|
||||||
|
return `${baseUrl}?${urlParams}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import invariant from "invariant";
|
import invariant from "invariant";
|
||||||
import { Op, WhereOptions } from "sequelize";
|
import { Op, WhereOptions } from "sequelize";
|
||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import { SHARE_URL_SLUG_REGEX } from "@shared/utils/urlHelpers";
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import {
|
import {
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
InvalidRequestError,
|
InvalidRequestError,
|
||||||
@@ -42,7 +42,7 @@ export default async function loadDocument({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shareUrlId =
|
const shareUrlId =
|
||||||
shareId && !isUUID(shareId) && SHARE_URL_SLUG_REGEX.test(shareId)
|
shareId && !isUUID(shareId) && UrlHelper.SHARE_URL_SLUG_REGEX.test(shareId)
|
||||||
? shareId
|
? shareId
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Table, TBody, TR, TD } from "oy-vey";
|
import { Table, TBody, TR, TD } from "oy-vey";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import theme from "@shared/styles/theme";
|
import theme from "@shared/styles/theme";
|
||||||
import { twitterUrl } from "@shared/utils/urlHelpers";
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -54,7 +54,7 @@ export default ({ unsubscribeUrl, children }: Props) => {
|
|||||||
<TR>
|
<TR>
|
||||||
<TD style={footerStyle}>
|
<TD style={footerStyle}>
|
||||||
<Link href={env.URL}>{env.APP_NAME}</Link>
|
<Link href={env.URL}>{env.APP_NAME}</Link>
|
||||||
<a href={twitterUrl()} style={externalLinkStyle}>
|
<a href={UrlHelper.twitter} style={externalLinkStyle}>
|
||||||
Twitter
|
Twitter
|
||||||
</a>
|
</a>
|
||||||
</TD>
|
</TD>
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ import {
|
|||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import type { CollectionSort } from "@shared/types";
|
import type { CollectionSort } from "@shared/types";
|
||||||
import { CollectionPermission, NavigationNode } from "@shared/types";
|
import { CollectionPermission, NavigationNode } from "@shared/types";
|
||||||
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||||
import slugify from "@shared/utils/slugify";
|
import slugify from "@shared/utils/slugify";
|
||||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
|
||||||
import { CollectionValidation } from "@shared/validations";
|
import { CollectionValidation } from "@shared/validations";
|
||||||
import { ValidationError } from "@server/errors";
|
import { ValidationError } from "@server/errors";
|
||||||
import Document from "./Document";
|
import Document from "./Document";
|
||||||
@@ -394,7 +394,7 @@ class Collection extends ParanoidModel<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = id.match(SLUG_URL_REGEX);
|
const match = id.match(UrlHelper.SLUG_URL_REGEX);
|
||||||
if (match) {
|
if (match) {
|
||||||
return this.findOne({
|
return this.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ import type {
|
|||||||
ProsemirrorData,
|
ProsemirrorData,
|
||||||
SourceMetadata,
|
SourceMetadata,
|
||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import getTasks from "@shared/utils/getTasks";
|
import getTasks from "@shared/utils/getTasks";
|
||||||
import slugify from "@shared/utils/slugify";
|
import slugify from "@shared/utils/slugify";
|
||||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
|
||||||
import { DocumentValidation } from "@shared/validations";
|
import { DocumentValidation } from "@shared/validations";
|
||||||
import { ValidationError } from "@server/errors";
|
import { ValidationError } from "@server/errors";
|
||||||
import Backlink from "./Backlink";
|
import Backlink from "./Backlink";
|
||||||
@@ -611,7 +611,7 @@ class Document extends ParanoidModel<
|
|||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = id.match(SLUG_URL_REGEX);
|
const match = id.match(UrlHelper.SLUG_URL_REGEX);
|
||||||
if (match) {
|
if (match) {
|
||||||
const document = await scope.findOne({
|
const document = await scope.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Unique,
|
Unique,
|
||||||
BeforeUpdate,
|
BeforeUpdate,
|
||||||
} from "sequelize-typescript";
|
} from "sequelize-typescript";
|
||||||
import { SHARE_URL_SLUG_REGEX } from "@shared/utils/urlHelpers";
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import { ValidationError } from "@server/errors";
|
import { ValidationError } from "@server/errors";
|
||||||
import Collection from "./Collection";
|
import Collection from "./Collection";
|
||||||
@@ -96,7 +96,7 @@ class Share extends IdModel<
|
|||||||
|
|
||||||
@AllowNull
|
@AllowNull
|
||||||
@Is({
|
@Is({
|
||||||
args: SHARE_URL_SLUG_REGEX,
|
args: UrlHelper.SHARE_URL_SLUG_REGEX,
|
||||||
msg: "Must be only alphanumeric and dashes",
|
msg: "Must be only alphanumeric and dashes",
|
||||||
})
|
})
|
||||||
@Column
|
@Column
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import isEmpty from "lodash/isEmpty";
|
|||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { DocumentPermission, StatusFilter } from "@shared/types";
|
import { DocumentPermission, StatusFilter } from "@shared/types";
|
||||||
import { SHARE_URL_SLUG_REGEX } from "@shared/utils/urlHelpers";
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import { BaseSchema } from "@server/routes/api/schema";
|
import { BaseSchema } from "@server/routes/api/schema";
|
||||||
|
|
||||||
const DocumentsSortParamsSchema = z.object({
|
const DocumentsSortParamsSchema = z.object({
|
||||||
@@ -115,7 +115,7 @@ export const DocumentsInfoSchema = BaseSchema.extend({
|
|||||||
/** Share Id, if available */
|
/** Share Id, if available */
|
||||||
shareId: z
|
shareId: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isUUID(val) || SHARE_URL_SLUG_REGEX.test(val))
|
.refine((val) => isUUID(val) || UrlHelper.SHARE_URL_SLUG_REGEX.test(val))
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
/** Version of the API to be used */
|
/** Version of the API to be used */
|
||||||
@@ -173,7 +173,7 @@ export const DocumentsSearchSchema = BaseSchema.extend({
|
|||||||
/** Filter results for the team derived from shareId */
|
/** Filter results for the team derived from shareId */
|
||||||
shareId: z
|
shareId: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isUUID(val) || SHARE_URL_SLUG_REGEX.test(val))
|
.refine((val) => isUUID(val) || UrlHelper.SHARE_URL_SLUG_REGEX.test(val))
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
/** Min words to be shown in the results snippets */
|
/** Min words to be shown in the results snippets */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import { BaseSchema } from "../schema";
|
import { BaseSchema } from "../schema";
|
||||||
|
|
||||||
export const PinsCreateSchema = BaseSchema.extend({
|
export const PinsCreateSchema = BaseSchema.extend({
|
||||||
@@ -9,7 +9,7 @@ export const PinsCreateSchema = BaseSchema.extend({
|
|||||||
.string({
|
.string({
|
||||||
required_error: "required",
|
required_error: "required",
|
||||||
})
|
})
|
||||||
.refine((val) => isUUID(val) || SLUG_URL_REGEX.test(val), {
|
.refine((val) => isUUID(val) || UrlHelper.SLUG_URL_REGEX.test(val), {
|
||||||
message: "must be uuid or url slug",
|
message: "must be uuid or url slug",
|
||||||
}),
|
}),
|
||||||
collectionId: z.string().uuid().nullish(),
|
collectionId: z.string().uuid().nullish(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SHARE_URL_SLUG_REGEX, SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import { Share } from "@server/models";
|
import { Share } from "@server/models";
|
||||||
import { BaseSchema } from "../schema";
|
import { BaseSchema } from "../schema";
|
||||||
|
|
||||||
@@ -13,7 +13,8 @@ export const SharesInfoSchema = BaseSchema.extend({
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.refine(
|
.refine(
|
||||||
(val) => (val ? isUUID(val) || SLUG_URL_REGEX.test(val) : true),
|
(val) =>
|
||||||
|
val ? isUUID(val) || UrlHelper.SLUG_URL_REGEX.test(val) : true,
|
||||||
{
|
{
|
||||||
message: "must be uuid or url slug",
|
message: "must be uuid or url slug",
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ export const SharesUpdateSchema = BaseSchema.extend({
|
|||||||
published: z.boolean().optional(),
|
published: z.boolean().optional(),
|
||||||
urlId: z
|
urlId: z
|
||||||
.string()
|
.string()
|
||||||
.regex(SHARE_URL_SLUG_REGEX, {
|
.regex(UrlHelper.SHARE_URL_SLUG_REGEX, {
|
||||||
message: "must contain only alphanumeric and dashes",
|
message: "must contain only alphanumeric and dashes",
|
||||||
})
|
})
|
||||||
.nullish(),
|
.nullish(),
|
||||||
@@ -65,13 +66,13 @@ export const SharesCreateSchema = BaseSchema.extend({
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
documentId: z
|
documentId: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isUUID(val) || SLUG_URL_REGEX.test(val), {
|
.refine((val) => isUUID(val) || UrlHelper.SLUG_URL_REGEX.test(val), {
|
||||||
message: "must be uuid or url slug",
|
message: "must be uuid or url slug",
|
||||||
}),
|
}),
|
||||||
published: z.boolean().default(false),
|
published: z.boolean().default(false),
|
||||||
urlId: z
|
urlId: z
|
||||||
.string()
|
.string()
|
||||||
.regex(SHARE_URL_SLUG_REGEX, {
|
.regex(UrlHelper.SHARE_URL_SLUG_REGEX, {
|
||||||
message: "must contain only alphanumeric and dashes",
|
message: "must contain only alphanumeric and dashes",
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import validator from "validator";
|
|||||||
import isIn from "validator/lib/isIn";
|
import isIn from "validator/lib/isIn";
|
||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import { CollectionPermission } from "@shared/types";
|
import { CollectionPermission } from "@shared/types";
|
||||||
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||||
import { validateColorHex } from "@shared/utils/color";
|
import { validateColorHex } from "@shared/utils/color";
|
||||||
import { validateIndexCharacters } from "@shared/utils/indexCharacters";
|
import { validateIndexCharacters } from "@shared/utils/indexCharacters";
|
||||||
import parseMentionUrl from "@shared/utils/parseMentionUrl";
|
import parseMentionUrl from "@shared/utils/parseMentionUrl";
|
||||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
|
||||||
import { isUrl } from "@shared/utils/urls";
|
import { isUrl } from "@shared/utils/urls";
|
||||||
import { ParamRequiredError, ValidationError } from "./errors";
|
import { ParamRequiredError, ValidationError } from "./errors";
|
||||||
import { Buckets } from "./models/helpers/AttachmentHelper";
|
import { Buckets } from "./models/helpers/AttachmentHelper";
|
||||||
@@ -210,7 +210,7 @@ export class ValidateDocumentId {
|
|||||||
* @returns true if documentId is valid, false otherwise
|
* @returns true if documentId is valid, false otherwise
|
||||||
*/
|
*/
|
||||||
public static isValid = (documentId: string) =>
|
public static isValid = (documentId: string) =>
|
||||||
isUUID(documentId) || SLUG_URL_REGEX.test(documentId);
|
isUUID(documentId) || UrlHelper.SLUG_URL_REGEX.test(documentId);
|
||||||
|
|
||||||
public static message = "Must be uuid or url slug";
|
public static message = "Must be uuid or url slug";
|
||||||
}
|
}
|
||||||
|
|||||||
10
shared/utils/UrlHelper.ts
Normal file
10
shared/utils/UrlHelper.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export class UrlHelper {
|
||||||
|
public static github = "https://www.github.com/outline/outline/issues";
|
||||||
|
public static twitter = "https://twitter.com/getoutline";
|
||||||
|
public static contact = "https://www.getoutline.com/contact";
|
||||||
|
public static developers = "https://www.getoutline.com/developers";
|
||||||
|
public static changelog = "https://www.getoutline.com/changelog";
|
||||||
|
|
||||||
|
public static SLUG_URL_REGEX = /^(?:[0-9a-zA-Z-_~]*-)?([a-zA-Z0-9]{10,15})$/;
|
||||||
|
public static SHARE_URL_SLUG_REGEX = /^[0-9a-z-]+$/;
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import env from "../env";
|
|
||||||
|
|
||||||
export function slackAuth(
|
|
||||||
state: string,
|
|
||||||
scopes: string[] = [
|
|
||||||
"identity.email",
|
|
||||||
"identity.basic",
|
|
||||||
"identity.avatar",
|
|
||||||
"identity.team",
|
|
||||||
],
|
|
||||||
clientId: string,
|
|
||||||
redirectUri = `${env.URL}/auth/slack.callback`
|
|
||||||
): string {
|
|
||||||
const baseUrl = "https://slack.com/oauth/authorize";
|
|
||||||
const params = {
|
|
||||||
client_id: clientId,
|
|
||||||
scope: scopes ? scopes.join(" ") : "",
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
state,
|
|
||||||
};
|
|
||||||
const urlParams = Object.keys(params)
|
|
||||||
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
|
||||||
.join("&");
|
|
||||||
return `${baseUrl}?${urlParams}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function githubUrl(): string {
|
|
||||||
return "https://www.github.com/outline";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function githubIssuesUrl(): string {
|
|
||||||
return "https://www.github.com/outline/outline/issues";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function twitterUrl(): string {
|
|
||||||
return "https://twitter.com/getoutline";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function feedbackUrl(): string {
|
|
||||||
return "https://www.getoutline.com/contact";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function developersUrl(): string {
|
|
||||||
return "https://www.getoutline.com/developers";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function changelogUrl(): string {
|
|
||||||
return "https://www.getoutline.com/changelog";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SLUG_URL_REGEX = /^(?:[0-9a-zA-Z-_~]*-)?([a-zA-Z0-9]{10,15})$/;
|
|
||||||
|
|
||||||
export const SHARE_URL_SLUG_REGEX = /^[0-9a-z-]+$/;
|
|
||||||
Reference in New Issue
Block a user