Skip to content

Commit

Permalink
Merge branch 'main' of github.com:outline/outline
Browse files Browse the repository at this point in the history
  • Loading branch information
tommoor committed Sep 24, 2022
2 parents 1ac33a9 + 91d8d27 commit 61a8230
Show file tree
Hide file tree
Showing 46 changed files with 752 additions and 295 deletions.
4 changes: 4 additions & 0 deletions app/models/WebhookSubscription.ts
Expand Up @@ -15,6 +15,10 @@ class WebhookSubscription extends BaseModel {
@observable
url: string;

@Field
@observable
secret: string;

@Field
@observable
enabled: boolean;
Expand Down
11 changes: 11 additions & 0 deletions app/scenes/Settings/components/WebhookSubscriptionForm.tsx
Expand Up @@ -146,6 +146,7 @@ type Props = {
interface FormData {
name: string;
url: string;
secret: string;
events: string[];
}

Expand All @@ -163,6 +164,7 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
events: webhookSubscription ? [...webhookSubscription.events] : [],
name: webhookSubscription?.name,
url: webhookSubscription?.url,
secret: webhookSubscription?.secret,
},
});

Expand Down Expand Up @@ -237,6 +239,7 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
autoFocus
flex
label={t("Name")}
placeholder={t("A memorable identifer")}
{...register("name", {
required: true,
})}
Expand All @@ -250,6 +253,14 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
label={t("URL")}
{...register("url", { required: true })}
/>
<ReactHookWrappedInput
flex
label={t("Secret") + ` (${t("Optional")})`}
placeholder={t("Used to sign payload")}
{...register("secret", {
required: false,
})}
/>
</TextFields>

<EventCheckbox label={t("All events")} value="*" />
Expand Down
1 change: 0 additions & 1 deletion app/typings/styled-components.d.ts
Expand Up @@ -16,7 +16,6 @@ declare module "styled-components" {
tableDivider: string;
tableSelected: string;
tableSelectedBackground: string;
tableHeaderBackground: string;
quote: string;
codeBackground: string;
codeBorder: string;
Expand Down
6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -163,7 +163,7 @@
"prosemirror-transform": "1.2.5",
"prosemirror-utils": "^0.9.6",
"prosemirror-view": "1.26.5",
"query-string": "^7.0.1",
"query-string": "^7.1.1",
"quoted-printable": "^1.0.1",
"randomstring": "1.1.5",
"rate-limiter-flexible": "^2.3.7",
Expand Down Expand Up @@ -306,7 +306,7 @@
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
"babel-plugin-transform-typescript-metadata": "^0.3.2",
"babel-plugin-tsconfig-paths-module-resolver": "^1.0.3",
"concurrently": "^7.3.0",
"concurrently": "^7.4.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.6",
"eslint": "^7.32.0",
Expand Down Expand Up @@ -335,7 +335,7 @@
"rimraf": "^2.5.4",
"terser-webpack-plugin": "^4.1.0",
"typescript": "^4.7.4",
"url-loader": "^0.6.2",
"url-loader": "^4.1.1",
"webpack": "4.44.1",
"webpack-cli": "^4.10.0",
"webpack-manifest-plugin": "^3.0.0",
Expand Down
14 changes: 2 additions & 12 deletions server/commands/revisionCreator.ts
Expand Up @@ -10,10 +10,7 @@ export default async function revisionCreator({
user: User;
ip?: string;
}) {
let transaction;

try {
transaction = await sequelize.transaction();
return sequelize.transaction(async (transaction) => {
const revision = await Revision.createFromDocument(document, {
transaction,
});
Expand All @@ -32,13 +29,6 @@ export default async function revisionCreator({
transaction,
}
);
await transaction.commit();
return revision;
} catch (err) {
if (transaction) {
await transaction.rollback();
}

throw err;
}
});
}
27 changes: 19 additions & 8 deletions server/emails/templates/DocumentNotificationEmail.tsx
Expand Up @@ -3,6 +3,7 @@ import { Document } from "@server/models";
import BaseEmail from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import Diff from "./components/Diff";
import EmailTemplate from "./components/EmailLayout";
import EmptySpace from "./components/EmptySpace";
import Footer from "./components/Footer";
Expand All @@ -17,6 +18,7 @@ type InputProps = {
eventName: string;
teamUrl: string;
unsubscribeUrl: string;
content: string;
};

type BeforeSend = {
Expand Down Expand Up @@ -73,25 +75,34 @@ Open Document: ${teamUrl}${document.url}
eventName = "published",
teamUrl,
unsubscribeUrl,
content,
}: Props) {
const link = `${teamUrl}${document.url}?ref=notification-email`;

return (
<EmailTemplate>
<Header />

<Body>
<Heading>
"{document.title}" {eventName}
{document.title} {eventName}
</Heading>
<p>
{actorName} {eventName} the document "{document.title}", in the{" "}
{collectionName} collection.
{actorName} {eventName} the document{" "}
<a href={link}>{document.title}</a>, in the {collectionName}{" "}
collection.
</p>
<hr />
<EmptySpace height={10} />
<p>{document.getSummary()}</p>
<EmptySpace height={10} />
{content && (
<>
<EmptySpace height={20} />
<Diff>
<div dangerouslySetInnerHTML={{ __html: content }} />
</Diff>
<EmptySpace height={20} />
</>
)}
<p>
<Button href={`${teamUrl}${document.url}`}>Open Document</Button>
<Button href={link}>Open Document</Button>
</p>
</Body>

Expand Down
2 changes: 1 addition & 1 deletion server/emails/templates/InviteEmail.tsx
Expand Up @@ -57,7 +57,7 @@ Join now: ${teamUrl}
</p>
<EmptySpace height={10} />
<p>
<Button href={teamUrl}>Join now</Button>
<Button href={`${teamUrl}?ref=invite-email`}>Join now</Button>
</p>
</Body>

Expand Down
4 changes: 3 additions & 1 deletion server/emails/templates/InviteReminderEmail.tsx
Expand Up @@ -59,7 +59,9 @@ If you haven't signed up yet, you can do so here: ${teamUrl}
<p>If you haven't signed up yet, you can do so here:</p>
<EmptySpace height={10} />
<p>
<Button href={teamUrl}>Join now</Button>
<Button href={`${teamUrl}?ref=invite-reminder-email`}>
Join now
</Button>
</p>
</Body>

Expand Down
4 changes: 3 additions & 1 deletion server/emails/templates/WelcomeEmail.tsx
Expand Up @@ -59,7 +59,9 @@ ${teamUrl}/home
</p>
<EmptySpace height={10} />
<p>
<Button href={`${teamUrl}/home`}>Open Outline</Button>
<Button href={`${teamUrl}/home?ref=welcome-email`}>
Open Outline
</Button>
</p>
</Body>

Expand Down
25 changes: 25 additions & 0 deletions server/emails/templates/components/Diff.tsx
@@ -0,0 +1,25 @@
import * as React from "react";
import theme from "@shared/styles/theme";

type Props = {
children: React.ReactNode;
href?: string;
};

export default ({ children, ...rest }: Props) => {
const style = {
borderRadius: "4px",
background: theme.secondaryBackground,
padding: ".75em 1em",
color: theme.text,
display: "block",
textDecoration: "none",
width: "100%",
};

return (
<div style={style} className="content-diff" {...rest}>
{children}
</div>
);
};
14 changes: 14 additions & 0 deletions server/migrations/20220922073737-webhook-signing-secret.js
@@ -0,0 +1,14 @@
"use strict";

module.exports = {
async up (queryInterface, Sequelize) {
return queryInterface.addColumn("webhook_subscriptions", "secret", {
type: Sequelize.BLOB,
allowNull: true,
});
},

async down (queryInterface, Sequelize) {
return queryInterface.removeColumn("webhook_subscriptions", "secret");
}
};
53 changes: 52 additions & 1 deletion server/models/WebhookSubscription.ts
@@ -1,4 +1,6 @@
import crypto from "crypto";
import { bool } from "aws-sdk/clients/signer";
import { isEmpty } from "lodash";
import {
Column,
Table,
Expand All @@ -9,6 +11,7 @@ import {
IsUrl,
BeforeCreate,
DefaultScope,
AllowNull,
} from "sequelize-typescript";
import { SaveOptions } from "sequelize/types";
import { WebhookSubscriptionValidation } from "@shared/validations";
Expand All @@ -17,6 +20,10 @@ import { Event } from "@server/types";
import Team from "./Team";
import User from "./User";
import ParanoidModel from "./base/ParanoidModel";
import Encrypted, {
setEncryptedColumn,
getEncryptedColumn,
} from "./decorators/Encrypted";
import Fix from "./decorators/Fix";
import Length from "./validators/Length";

Expand Down Expand Up @@ -51,6 +58,21 @@ class WebhookSubscription extends ParanoidModel {
@Column(DataType.ARRAY(DataType.STRING))
events: string[];

@AllowNull
@Encrypted
@Column(DataType.BLOB)
get secret() {
const val = getEncryptedColumn(this, "secret");
// Turns out that `val` evals to `{}` instead
// of `null` even if secret's value in db is `null`.
// https://github.com/defunctzombie/sequelize-encrypted/blob/c3854e76ae4b80318c8f10f94e6c898c67659ca6/index.js#L30-L33 explains it possibly.
return isEmpty(val) ? "" : val;
}

set secret(value: string) {
setEncryptedColumn(this, "secret", value);
}

// associations

@BelongsTo(() => User, "createdById")
Expand Down Expand Up @@ -87,12 +109,19 @@ class WebhookSubscription extends ParanoidModel {
* Disables the webhook subscription
*
* @param options Save options
* @returns Promise<void>
* @returns Promise<WebhookSubscription>
*/
public async disable(options?: SaveOptions<WebhookSubscription>) {
return this.update({ enabled: false }, options);
}

/**
* Determines if an event should be processed for this webhook subscription
* based on the event configuration.
*
* @param event Event to ceck
* @returns true if event is valid
*/
public validForEvent = (event: Event): bool => {
if (this.events.length === 1 && this.events[0] === "*") {
return true;
Expand All @@ -106,6 +135,28 @@ class WebhookSubscription extends ParanoidModel {

return false;
};

/**
* Calculates the signature for a webhook payload if the webhook subscription
* has an associated secret stored.
*
* @param payload The text payload of a webhook delivery
* @returns the signature as a string
*/
public signature = (payload: string) => {
if (isEmpty(this.secret)) {
return;
}

const signTimestamp = Date.now();

const signature = crypto
.createHmac("sha256", this.secret)
.update(`${signTimestamp}.${payload}`)
.digest("hex");

return `t=${signTimestamp},s=${signature}`;
};
}

export default WebhookSubscription;

0 comments on commit 61a8230

Please sign in to comment.