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:
Apoorv Mishra
2024-02-29 11:41:03 +05:30
committed by GitHub
parent 021cd253af
commit fd34a6d19c
20 changed files with 146 additions and 127 deletions

View File

@@ -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({

View File

@@ -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() {

View File

@@ -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")
); );

View File

@@ -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"`
);
}
}), }),
]); ]);

View File

@@ -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 />}
/> />

View File

@@ -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 (

View File

@@ -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);
} }
); );
} }

View File

@@ -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,

View 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}`;
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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

View File

@@ -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 */

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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
View 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-]+$/;
}

View File

@@ -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-]+$/;