Merge branch 'develop' of github.com:outline/outline into feat/mass-import

This commit is contained in:
Tom Moor
2020-12-22 20:43:58 -08:00
23 changed files with 333 additions and 181 deletions

View File

@@ -98,11 +98,18 @@ router.post("attachments.delete", auth(), async (ctx) => {
const user = ctx.state.user;
const attachment = await Attachment.findByPk(id);
const document = await Document.findByPk(attachment.documentId, {
userId: user.id,
});
authorize(user, "update", document);
if (!attachment) {
throw new NotFoundError();
}
if (attachment.documentId) {
const document = await Document.findByPk(attachment.documentId, {
userId: user.id,
});
authorize(user, "update", document);
}
authorize(user, "delete", attachment);
await attachment.destroy();
await Event.create({

View File

@@ -43,6 +43,71 @@ describe("#attachments.delete", () => {
expect(await Attachment.count()).toEqual(0);
});
it("should allow deleting an attachment without a document created by user", async () => {
const user = await buildUser();
const attachment = await buildAttachment({
teamId: user.teamId,
userId: user.id,
});
attachment.documentId = null;
await attachment.save();
const res = await server.post("/api/attachments.delete", {
body: { token: user.getJwtToken(), id: attachment.id },
});
expect(res.status).toEqual(200);
expect(await Attachment.count()).toEqual(0);
});
it("should allow deleting an attachment without a document if admin", async () => {
const user = await buildUser({ isAdmin: true });
const attachment = await buildAttachment({
teamId: user.teamId,
});
attachment.documentId = null;
await attachment.save();
const res = await server.post("/api/attachments.delete", {
body: { token: user.getJwtToken(), id: attachment.id },
});
expect(res.status).toEqual(200);
expect(await Attachment.count()).toEqual(0);
});
it("should not allow deleting an attachment in another team", async () => {
const user = await buildUser({ isAdmin: true });
const attachment = await buildAttachment();
attachment.documentId = null;
await attachment.save();
const res = await server.post("/api/attachments.delete", {
body: { token: user.getJwtToken(), id: attachment.id },
});
expect(res.status).toEqual(403);
});
it("should not allow deleting an attachment without a document", async () => {
const user = await buildUser();
const attachment = await buildAttachment({
teamId: user.teamId,
});
attachment.documentId = null;
await attachment.save();
const res = await server.post("/api/attachments.delete", {
body: { token: user.getJwtToken(), id: attachment.id },
});
expect(res.status).toEqual(403);
});
it("should not allow deleting an attachment belonging to a document user does not have access to", async () => {
const user = await buildUser();
const collection = await buildCollection({

View File

@@ -31,13 +31,13 @@ const api = new Koa();
const router = new Router();
// middlewares
api.use(errorHandling());
api.use(
bodyParser({
multipart: true,
formidable: { maxFieldsSize: 10 * 1024 * 1024 },
})
);
api.use(errorHandling());
api.use(methodOverride());
api.use(validation());
api.use(apiWrapper());

View File

@@ -45,7 +45,11 @@ router.post("email", async (ctx) => {
user.lastSigninEmailSentAt &&
user.lastSigninEmailSentAt > subMinutes(new Date(), 2)
) {
ctx.redirect(`${team.url}?notice=email-auth-ratelimit`);
ctx.body = {
redirect: `${team.url}?notice=email-auth-ratelimit`,
message: "Rate limit exceeded",
success: false,
};
return;
}

View File

@@ -26,6 +26,8 @@ import { baseStyles } from "./emails/components/EmailLayout";
import { createQueue } from "./utils/queue";
const log = debug("emails");
const useTestEmailService =
process.env.NODE_ENV !== "production" && !process.env.SMTP_USERNAME;
type Emails = "welcome" | "export";
@@ -73,7 +75,7 @@ export class Mailer {
try {
log(`Sending email "${data.title}" to ${data.to}`);
await transporter.sendMail({
const info = await transporter.sendMail({
from: process.env.SMTP_FROM_EMAIL,
replyTo: process.env.SMTP_REPLY_EMAIL || process.env.SMTP_FROM_EMAIL,
to: data.to,
@@ -82,6 +84,10 @@ export class Mailer {
text: data.text,
attachments: data.attachments,
});
if (useTestEmailService) {
log("Email Preview URL: %s", nodemailer.getTestMessageUrl(info));
}
} catch (err) {
if (process.env.SENTRY_DSN) {
Sentry.captureException(err);
@@ -159,6 +165,10 @@ export class Mailer {
};
constructor() {
this.loadTransport();
}
async loadTransport() {
if (process.env.SMTP_HOST) {
let smtpConfig = {
host: process.env.SMTP_HOST,
@@ -174,6 +184,24 @@ export class Mailer {
};
}
this.transporter = nodemailer.createTransport(smtpConfig);
return;
}
if (useTestEmailService) {
log("SMTP_USERNAME not provided, generating test account…");
let testAccount = await nodemailer.createTestAccount();
const smtpConfig = {
host: "smtp.ethereal.email",
port: 587,
secure: false,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
};
this.transporter = nodemailer.createTransport(smtpConfig);
}
}

View File

@@ -0,0 +1,14 @@
// @flow
import { Attachment, User } from "../models";
import policy from "./policy";
const { allow } = policy;
allow(User, "create", Attachment);
allow(User, "delete", Attachment, (actor, attachment) => {
if (!attachment || attachment.teamId !== actor.teamId) return false;
if (actor.isAdmin) return true;
if (actor.id === attachment.userId) return true;
return false;
});

View File

@@ -1,7 +1,8 @@
// @flow
import { Team, User, Collection, Document, Group } from "../models";
import { Attachment, Team, User, Collection, Document, Group } from "../models";
import policy from "./policy";
import "./apiKey";
import "./attachment";
import "./collection";
import "./document";
import "./integration";
@@ -24,7 +25,7 @@ type Policy = {
*/
export function serialize(
model: User,
target: Team | Collection | Document | Group
target: Attachment | Team | Collection | Document | Group
): Policy {
let output = {};

View File

@@ -8,7 +8,6 @@ import sendfile from "koa-sendfile";
import serve from "koa-static";
import { languages } from "../shared/i18n";
import environment from "./env";
import { NotFoundError } from "./errors";
import apexRedirect from "./middlewares/apexRedirect";
import { opensearchResponse } from "./utils/opensearch";
import { robotsResponse } from "./utils/robots";
@@ -78,7 +77,8 @@ router.get("/locales/:lng.json", async (ctx) => {
let { lng } = ctx.params;
if (!languages.includes(lng)) {
throw new NotFoundError();
ctx.status = 404;
return;
}
if (process.env.NODE_ENV === "production") {

View File

@@ -54,9 +54,11 @@
ignoreErrors: [
"ResizeObserver loop limit exceeded",
"AuthorizationError",
"BadRequestError",
"NetworkError",
"NotFoundError",
"OfflineError",
"ServiceUnavailableError",
"UpdateRequiredError",
"ChunkLoadError",
],