chore: Move to prettier standard double quotes (#1309)

This commit is contained in:
Tom Moor
2020-06-20 13:59:15 -07:00
committed by GitHub
parent 2a3b9e2104
commit f43deb7940
444 changed files with 5988 additions and 5977 deletions

View File

@@ -1,21 +1,21 @@
// @flow
import Router from 'koa-router';
import Router from "koa-router";
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import { presentApiKey } from '../presenters';
import { ApiKey, Event } from '../models';
import policy from '../policies';
import auth from "../middlewares/authentication";
import pagination from "./middlewares/pagination";
import { presentApiKey } from "../presenters";
import { ApiKey, Event } from "../models";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('apiKeys.create', auth(), async ctx => {
router.post("apiKeys.create", auth(), async ctx => {
const { name } = ctx.body;
ctx.assertPresent(name, 'name is required');
ctx.assertPresent(name, "name is required");
const user = ctx.state.user;
authorize(user, 'create', ApiKey);
authorize(user, "create", ApiKey);
const key = await ApiKey.create({
name,
@@ -23,7 +23,7 @@ router.post('apiKeys.create', auth(), async ctx => {
});
await Event.create({
name: 'api_keys.create',
name: "api_keys.create",
modelId: key.id,
teamId: user.teamId,
actorId: user.id,
@@ -36,13 +36,13 @@ router.post('apiKeys.create', auth(), async ctx => {
};
});
router.post('apiKeys.list', auth(), pagination(), async ctx => {
router.post("apiKeys.list", auth(), pagination(), async ctx => {
const user = ctx.state.user;
const keys = await ApiKey.findAll({
where: {
userId: user.id,
},
order: [['createdAt', 'DESC']],
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
@@ -53,18 +53,18 @@ router.post('apiKeys.list', auth(), pagination(), async ctx => {
};
});
router.post('apiKeys.delete', auth(), async ctx => {
router.post("apiKeys.delete", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const key = await ApiKey.findByPk(id);
authorize(user, 'delete', key);
authorize(user, "delete", key);
await key.destroy();
await Event.create({
name: 'api_keys.delete',
name: "api_keys.delete",
modelId: key.id,
teamId: user.teamId,
actorId: user.id,

View File

@@ -1,29 +1,29 @@
// @flow
import Router from 'koa-router';
import uuid from 'uuid';
import format from 'date-fns/format';
import { Attachment, Document, Event } from '../models';
import Router from "koa-router";
import uuid from "uuid";
import format from "date-fns/format";
import { Attachment, Document, Event } from "../models";
import {
makePolicy,
getSignature,
publicS3Endpoint,
makeCredential,
getSignedImageUrl,
} from '../utils/s3';
import auth from '../middlewares/authentication';
import { NotFoundError } from '../errors';
import policy from '../policies';
} from "../utils/s3";
import auth from "../middlewares/authentication";
import { NotFoundError } from "../errors";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
const AWS_S3_ACL = process.env.AWS_S3_ACL || 'private';
const AWS_S3_ACL = process.env.AWS_S3_ACL || "private";
router.post('attachments.create', auth(), async ctx => {
router.post("attachments.create", auth(), async ctx => {
let { name, documentId, contentType, size } = ctx.body;
ctx.assertPresent(name, 'name is required');
ctx.assertPresent(contentType, 'contentType is required');
ctx.assertPresent(size, 'size is required');
ctx.assertPresent(name, "name is required");
ctx.assertPresent(contentType, "contentType is required");
ctx.assertPresent(size, "size is required");
const { user } = ctx.state;
const s3Key = uuid.v4();
@@ -31,16 +31,16 @@ router.post('attachments.create', auth(), async ctx => {
const acl =
ctx.body.public === undefined
? AWS_S3_ACL
: ctx.body.public ? 'public-read' : 'private';
: ctx.body.public ? "public-read" : "private";
const credential = makeCredential();
const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z');
const longDate = format(new Date(), "YYYYMMDDTHHmmss\\Z");
const policy = makePolicy(credential, longDate, acl);
const endpoint = publicS3Endpoint();
const url = `${endpoint}/${key}`;
if (documentId) {
const document = await Document.findByPk(documentId, { userId: user.id });
authorize(user, 'update', document);
authorize(user, "update", document);
}
const attachment = await Attachment.create({
@@ -55,7 +55,7 @@ router.post('attachments.create', auth(), async ctx => {
});
await Event.create({
name: 'attachments.create',
name: "attachments.create",
data: { name },
teamId: user.teamId,
userId: user.id,
@@ -67,15 +67,15 @@ router.post('attachments.create', auth(), async ctx => {
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
uploadUrl: endpoint,
form: {
'Cache-Control': 'max-age=31557600',
'Content-Type': contentType,
"Cache-Control": "max-age=31557600",
"Content-Type": contentType,
acl,
key,
policy,
'x-amz-algorithm': 'AWS4-HMAC-SHA256',
'x-amz-credential': credential,
'x-amz-date': longDate,
'x-amz-signature': getSignature(policy),
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-credential": credential,
"x-amz-date": longDate,
"x-amz-signature": getSignature(policy),
},
attachment: {
documentId,
@@ -88,9 +88,9 @@ router.post('attachments.create', auth(), async ctx => {
};
});
router.post('attachments.redirect', auth(), async ctx => {
router.post("attachments.redirect", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const attachment = await Attachment.findByPk(id);
@@ -103,7 +103,7 @@ router.post('attachments.redirect', auth(), async ctx => {
const document = await Document.findByPk(attachment.documentId, {
userId: user.id,
});
authorize(user, 'read', document);
authorize(user, "read", document);
}
const accessUrl = await getSignedImageUrl(attachment.key);

View File

@@ -1,40 +1,40 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { flushdb } from '../test/support';
import TestServer from "fetch-test-server";
import app from "../app";
import { flushdb } from "../test/support";
import {
buildUser,
buildCollection,
buildAttachment,
buildDocument,
} from '../test/factories';
} from "../test/factories";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#attachments.redirect', async () => {
it('should require authentication', async () => {
const res = await server.post('/api/attachments.redirect');
describe("#attachments.redirect", async () => {
it("should require authentication", async () => {
const res = await server.post("/api/attachments.redirect");
expect(res.status).toEqual(401);
});
it('should return a redirect for an attachment belonging to a document user has access to', async () => {
it("should return a redirect for an attachment belonging to a document user has access to", async () => {
const user = await buildUser();
const attachment = await buildAttachment({
teamId: user.teamId,
userId: user.id,
});
const res = await server.post('/api/attachments.redirect', {
const res = await server.post("/api/attachments.redirect", {
body: { token: user.getJwtToken(), id: attachment.id },
redirect: 'manual',
redirect: "manual",
});
expect(res.status).toEqual(302);
});
it('should always return a redirect for a public attachment', async () => {
it("should always return a redirect for a public attachment", async () => {
const user = await buildUser();
const collection = await buildCollection({
teamId: user.teamId,
@@ -52,15 +52,15 @@ describe('#attachments.redirect', async () => {
documentId: document.id,
});
const res = await server.post('/api/attachments.redirect', {
const res = await server.post("/api/attachments.redirect", {
body: { token: user.getJwtToken(), id: attachment.id },
redirect: 'manual',
redirect: "manual",
});
expect(res.status).toEqual(302);
});
it('should not return a redirect for a private attachment belonging to a document user does not have access to', async () => {
it("should not return a redirect for a private attachment belonging to a document user does not have access to", async () => {
const user = await buildUser();
const collection = await buildCollection({
private: true,
@@ -74,10 +74,10 @@ describe('#attachments.redirect', async () => {
teamId: document.teamId,
userId: document.userId,
documentId: document.id,
acl: 'private',
acl: "private",
});
const res = await server.post('/api/attachments.redirect', {
const res = await server.post("/api/attachments.redirect", {
body: { token: user.getJwtToken(), id: attachment.id },
});

View File

@@ -1,12 +1,12 @@
// @flow
import Router from 'koa-router';
import auth from '../middlewares/authentication';
import { presentUser, presentTeam, presentPolicies } from '../presenters';
import { Team } from '../models';
import Router from "koa-router";
import auth from "../middlewares/authentication";
import { presentUser, presentTeam, presentPolicies } from "../presenters";
import { Team } from "../models";
const router = new Router();
router.post('auth.info', auth(), async ctx => {
router.post("auth.info", auth(), async ctx => {
const user = ctx.state.user;
const team = await Team.findByPk(user.teamId);

View File

@@ -1,9 +1,9 @@
// @flow
import fs from 'fs';
import Router from 'koa-router';
import { Op } from '../sequelize';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import fs from "fs";
import Router from "koa-router";
import { Op } from "../sequelize";
import auth from "../middlewares/authentication";
import pagination from "./middlewares/pagination";
import {
presentCollection,
presentUser,
@@ -11,7 +11,7 @@ import {
presentMembership,
presentGroup,
presentCollectionGroupMembership,
} from '../presenters';
} from "../presenters";
import {
Collection,
CollectionUser,
@@ -20,40 +20,40 @@ import {
Event,
User,
Group,
} from '../models';
import { ValidationError } from '../errors';
import { exportCollections } from '../logistics';
import { archiveCollection, archiveCollections } from '../utils/zip';
import policy from '../policies';
} from "../models";
import { ValidationError } from "../errors";
import { exportCollections } from "../logistics";
import { archiveCollection, archiveCollections } from "../utils/zip";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('collections.create', auth(), async ctx => {
router.post("collections.create", auth(), async ctx => {
const { name, color, description, icon, type } = ctx.body;
const isPrivate = ctx.body.private;
ctx.assertPresent(name, 'name is required');
ctx.assertPresent(name, "name is required");
if (color) {
ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)');
ctx.assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
}
const user = ctx.state.user;
authorize(user, 'create', Collection);
authorize(user, "create", Collection);
let collection = await Collection.create({
name,
description,
icon,
color,
type: type || 'atlas',
type: type || "atlas",
teamId: user.teamId,
creatorId: user.id,
private: isPrivate,
});
await Event.create({
name: 'collections.create',
name: "collections.create",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,
@@ -64,7 +64,7 @@ router.post('collections.create', auth(), async ctx => {
// we must reload the collection to get memberships for policy presenter
if (isPrivate) {
collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(collection.id);
}
@@ -74,15 +74,15 @@ router.post('collections.create', auth(), async ctx => {
};
});
router.post('collections.info', auth(), async ctx => {
router.post("collections.info", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, 'read', collection);
authorize(user, "read", collection);
ctx.body = {
data: presentCollection(collection),
@@ -90,18 +90,18 @@ router.post('collections.info', auth(), async ctx => {
};
});
router.post('collections.add_group', auth(), async ctx => {
const { id, groupId, permission = 'read_write' } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(groupId, 'groupId is required');
router.post("collections.add_group", auth(), async ctx => {
const { id, groupId, permission = "read_write" } = ctx.body;
ctx.assertUuid(id, "id is required");
ctx.assertUuid(groupId, "groupId is required");
const collection = await Collection.scope({
method: ['withMembership', ctx.state.user.id],
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, 'update', collection);
authorize(ctx.state.user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(ctx.state.user, 'read', group);
authorize(ctx.state.user, "read", group);
let membership = await CollectionGroup.findOne({
where: {
@@ -123,7 +123,7 @@ router.post('collections.add_group', auth(), async ctx => {
}
await Event.create({
name: 'collections.add_group',
name: "collections.add_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
@@ -140,23 +140,23 @@ router.post('collections.add_group', auth(), async ctx => {
};
});
router.post('collections.remove_group', auth(), async ctx => {
router.post("collections.remove_group", auth(), async ctx => {
const { id, groupId } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(groupId, 'groupId is required');
ctx.assertUuid(id, "id is required");
ctx.assertUuid(groupId, "groupId is required");
const collection = await Collection.scope({
method: ['withMembership', ctx.state.user.id],
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, 'update', collection);
authorize(ctx.state.user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(ctx.state.user, 'read', group);
authorize(ctx.state.user, "read", group);
await collection.removeGroup(group);
await Event.create({
name: 'collections.remove_group',
name: "collections.remove_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
@@ -170,19 +170,19 @@ router.post('collections.remove_group', auth(), async ctx => {
});
router.post(
'collections.group_memberships',
"collections.group_memberships",
auth(),
pagination(),
async ctx => {
const { id, query, permission } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, 'read', collection);
authorize(user, "read", collection);
let where = {
collectionId: id,
@@ -207,13 +207,13 @@ router.post(
const memberships = await CollectionGroup.findAll({
where,
order: [['createdAt', 'DESC']],
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
include: [
{
model: Group,
as: 'group',
as: "group",
where: groupWhere,
required: true,
},
@@ -232,18 +232,18 @@ router.post(
}
);
router.post('collections.add_user', auth(), async ctx => {
const { id, userId, permission = 'read_write' } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(userId, 'userId is required');
router.post("collections.add_user", auth(), async ctx => {
const { id, userId, permission = "read_write" } = ctx.body;
ctx.assertUuid(id, "id is required");
ctx.assertUuid(userId, "userId is required");
const collection = await Collection.scope({
method: ['withMembership', ctx.state.user.id],
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, 'update', collection);
authorize(ctx.state.user, "update", collection);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'read', user);
authorize(ctx.state.user, "read", user);
let membership = await CollectionUser.findOne({
where: {
@@ -265,7 +265,7 @@ router.post('collections.add_user', auth(), async ctx => {
}
await Event.create({
name: 'collections.add_user',
name: "collections.add_user",
userId,
collectionId: collection.id,
teamId: collection.teamId,
@@ -282,23 +282,23 @@ router.post('collections.add_user', auth(), async ctx => {
};
});
router.post('collections.remove_user', auth(), async ctx => {
router.post("collections.remove_user", auth(), async ctx => {
const { id, userId } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(userId, 'userId is required');
ctx.assertUuid(id, "id is required");
ctx.assertUuid(userId, "userId is required");
const collection = await Collection.scope({
method: ['withMembership', ctx.state.user.id],
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, 'update', collection);
authorize(ctx.state.user, "update", collection);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'read', user);
authorize(ctx.state.user, "read", user);
await collection.removeUser(user);
await Event.create({
name: 'collections.remove_user',
name: "collections.remove_user",
userId,
collectionId: collection.id,
teamId: collection.teamId,
@@ -313,15 +313,15 @@ router.post('collections.remove_user', auth(), async ctx => {
});
// DEPRECATED: Use collection.memberships which has pagination, filtering and permissions
router.post('collections.users', auth(), async ctx => {
router.post("collections.users", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, 'read', collection);
authorize(user, "read", collection);
const users = await collection.getUsers();
@@ -330,15 +330,15 @@ router.post('collections.users', auth(), async ctx => {
};
});
router.post('collections.memberships', auth(), pagination(), async ctx => {
router.post("collections.memberships", auth(), pagination(), async ctx => {
const { id, query, permission } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, 'read', collection);
authorize(user, "read", collection);
let where = {
collectionId: id,
@@ -363,13 +363,13 @@ router.post('collections.memberships', auth(), pagination(), async ctx => {
const memberships = await CollectionUser.findAll({
where,
order: [['createdAt', 'DESC']],
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
include: [
{
model: User,
as: 'user',
as: "user",
where: userWhere,
required: true,
},
@@ -385,20 +385,20 @@ router.post('collections.memberships', auth(), pagination(), async ctx => {
};
});
router.post('collections.export', auth(), async ctx => {
router.post("collections.export", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, 'export', collection);
authorize(user, "export", collection);
const filePath = await archiveCollection(collection);
await Event.create({
name: 'collections.export',
name: "collections.export",
collectionId: collection.id,
teamId: user.teamId,
actorId: user.id,
@@ -407,19 +407,19 @@ router.post('collections.export', auth(), async ctx => {
});
ctx.attachment(`${collection.name}.zip`);
ctx.set('Content-Type', 'application/force-download');
ctx.set("Content-Type", "application/force-download");
ctx.body = fs.createReadStream(filePath);
});
router.post('collections.export_all', auth(), async ctx => {
router.post("collections.export_all", auth(), async ctx => {
const { download = false } = ctx.body;
const user = ctx.state.user;
const team = await Team.findByPk(user.teamId);
authorize(user, 'export', team);
authorize(user, "export", team);
await Event.create({
name: 'collections.export',
name: "collections.export",
teamId: user.teamId,
actorId: user.id,
ip: ctx.request.ip,
@@ -428,12 +428,12 @@ router.post('collections.export_all', auth(), async ctx => {
if (download) {
const collections = await Collection.findAll({
where: { teamId: team.id },
order: [['name', 'ASC']],
order: [["name", "ASC"]],
});
const filePath = await archiveCollections(collections);
ctx.attachment(`${team.name}.zip`);
ctx.set('Content-Type', 'application/force-download');
ctx.set("Content-Type", "application/force-download");
ctx.body = fs.createReadStream(filePath);
} else {
// async operation to create zip archive and email user
@@ -445,22 +445,22 @@ router.post('collections.export_all', auth(), async ctx => {
}
});
router.post('collections.update', auth(), async ctx => {
router.post("collections.update", auth(), async ctx => {
const { id, name, description, icon, color } = ctx.body;
const isPrivate = ctx.body.private;
ctx.assertPresent(name, 'name is required');
ctx.assertPresent(name, "name is required");
if (color) {
ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)');
ctx.assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
}
const user = ctx.state.user;
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, 'update', collection);
authorize(user, "update", collection);
// we're making this collection private right now, ensure that the current
// user has a read-write membership so that at least they can edit it
@@ -471,7 +471,7 @@ router.post('collections.update', auth(), async ctx => {
userId: user.id,
},
defaults: {
permission: 'read_write',
permission: "read_write",
createdById: user.id,
},
});
@@ -488,7 +488,7 @@ router.post('collections.update', auth(), async ctx => {
await collection.save();
await Event.create({
name: 'collections.update',
name: "collections.update",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,
@@ -508,17 +508,17 @@ router.post('collections.update', auth(), async ctx => {
};
});
router.post('collections.list', auth(), pagination(), async ctx => {
router.post("collections.list", auth(), pagination(), async ctx => {
const user = ctx.state.user;
const collectionIds = await user.collectionIds();
let collections = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findAll({
where: {
teamId: user.teamId,
id: collectionIds,
},
order: [['updatedAt', 'DESC']],
order: [["updatedAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
@@ -530,24 +530,24 @@ router.post('collections.list', auth(), pagination(), async ctx => {
};
});
router.post('collections.delete', auth(), async ctx => {
router.post("collections.delete", auth(), async ctx => {
const { id } = ctx.body;
const user = ctx.state.user;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, 'delete', collection);
authorize(user, "delete", collection);
const total = await Collection.count();
if (total === 1) throw new ValidationError('Cannot delete last collection');
if (total === 1) throw new ValidationError("Cannot delete last collection");
await collection.destroy();
await Event.create({
name: 'collections.delete',
name: "collections.delete",
collectionId: collection.id,
teamId: collection.teamId,
actorId: user.id,

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
// @flow
import Router from 'koa-router';
import Sequelize from 'sequelize';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import documentMover from '../commands/documentMover';
import Router from "koa-router";
import Sequelize from "sequelize";
import auth from "../middlewares/authentication";
import pagination from "./middlewares/pagination";
import documentMover from "../commands/documentMover";
import {
presentDocument,
presentCollection,
presentPolicies,
} from '../presenters';
} from "../presenters";
import {
Collection,
Document,
@@ -19,23 +19,23 @@ import {
Revision,
Backlink,
User,
} from '../models';
import { InvalidRequestError } from '../errors';
import policy from '../policies';
import { sequelize } from '../sequelize';
} from "../models";
import { InvalidRequestError } from "../errors";
import policy from "../policies";
import { sequelize } from "../sequelize";
const Op = Sequelize.Op;
const { authorize, cannot } = policy;
const router = new Router();
router.post('documents.list', auth(), pagination(), async ctx => {
const { sort = 'updatedAt', backlinkDocumentId, parentDocumentId } = ctx.body;
router.post("documents.list", auth(), pagination(), async ctx => {
const { sort = "updatedAt", backlinkDocumentId, parentDocumentId } = ctx.body;
// collection and user are here for backwards compatablity
const collectionId = ctx.body.collectionId || ctx.body.collection;
const createdById = ctx.body.userId || ctx.body.user;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
if (direction !== "ASC") direction = "DESC";
// always filter by the current team
const user = ctx.state.user;
@@ -44,19 +44,19 @@ router.post('documents.list', auth(), pagination(), async ctx => {
// if a specific user is passed then add to filters. If the user doesn't
// exist in the team then nothing will be returned, so no need to check auth
if (createdById) {
ctx.assertUuid(createdById, 'user must be a UUID');
ctx.assertUuid(createdById, "user must be a UUID");
where = { ...where, createdById };
}
// if a specific collection is passed then we need to check auth to view it
if (collectionId) {
ctx.assertUuid(collectionId, 'collection must be a UUID');
ctx.assertUuid(collectionId, "collection must be a UUID");
where = { ...where, collectionId };
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(collectionId);
authorize(user, 'read', collection);
authorize(user, "read", collection);
// otherwise, filter by all collections the user has access to
} else {
@@ -65,15 +65,15 @@ router.post('documents.list', auth(), pagination(), async ctx => {
}
if (parentDocumentId) {
ctx.assertUuid(parentDocumentId, 'parentDocumentId must be a UUID');
ctx.assertUuid(parentDocumentId, "parentDocumentId must be a UUID");
where = { ...where, parentDocumentId };
}
if (backlinkDocumentId) {
ctx.assertUuid(backlinkDocumentId, 'backlinkDocumentId must be a UUID');
ctx.assertUuid(backlinkDocumentId, "backlinkDocumentId must be a UUID");
const backlinks = await Backlink.findAll({
attributes: ['reverseDocumentId'],
attributes: ["reverseDocumentId"],
where: {
documentId: backlinkDocumentId,
},
@@ -86,10 +86,10 @@ router.post('documents.list', auth(), pagination(), async ctx => {
}
// add the users starred state to the response by default
const starredScope = { method: ['withStarred', user.id] };
const collectionScope = { method: ['withCollection', user.id] };
const starredScope = { method: ["withStarred", user.id] };
const collectionScope = { method: ["withCollection", user.id] };
const documents = await Document.scope(
'defaultScope',
"defaultScope",
starredScope,
collectionScope
).findAll({
@@ -112,23 +112,23 @@ router.post('documents.list', auth(), pagination(), async ctx => {
};
});
router.post('documents.pinned', auth(), pagination(), async ctx => {
const { collectionId, sort = 'updatedAt' } = ctx.body;
router.post("documents.pinned", auth(), pagination(), async ctx => {
const { collectionId, sort = "updatedAt" } = ctx.body;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
ctx.assertUuid(collectionId, 'collectionId is required');
if (direction !== "ASC") direction = "DESC";
ctx.assertUuid(collectionId, "collectionId is required");
const user = ctx.state.user;
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(collectionId);
authorize(user, 'read', collection);
authorize(user, "read", collection);
const starredScope = { method: ['withStarred', user.id] };
const collectionScope = { method: ['withCollection', user.id] };
const starredScope = { method: ["withStarred", user.id] };
const collectionScope = { method: ["withCollection", user.id] };
const documents = await Document.scope(
'defaultScope',
"defaultScope",
starredScope,
collectionScope
).findAll({
@@ -157,17 +157,17 @@ router.post('documents.pinned', auth(), pagination(), async ctx => {
};
});
router.post('documents.archived', auth(), pagination(), async ctx => {
const { sort = 'updatedAt' } = ctx.body;
router.post("documents.archived", auth(), pagination(), async ctx => {
const { sort = "updatedAt" } = ctx.body;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const collectionIds = await user.collectionIds();
const collectionScope = { method: ['withCollection', user.id] };
const collectionScope = { method: ["withCollection", user.id] };
const documents = await Document.scope(
'defaultScope',
"defaultScope",
collectionScope
).findAll({
where: {
@@ -195,15 +195,15 @@ router.post('documents.archived', auth(), pagination(), async ctx => {
};
});
router.post('documents.deleted', auth(), pagination(), async ctx => {
const { sort = 'deletedAt' } = ctx.body;
router.post("documents.deleted", auth(), pagination(), async ctx => {
const { sort = "deletedAt" } = ctx.body;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const collectionIds = await user.collectionIds();
const collectionScope = { method: ['withCollection', user.id] };
const collectionScope = { method: ["withCollection", user.id] };
const documents = await Document.scope(collectionScope).findAll({
where: {
teamId: user.teamId,
@@ -213,8 +213,8 @@ router.post('documents.deleted', auth(), pagination(), async ctx => {
},
},
include: [
{ model: User, as: 'createdBy', paranoid: false },
{ model: User, as: 'updatedBy', paranoid: false },
{ model: User, as: "createdBy", paranoid: false },
{ model: User, as: "updatedBy", paranoid: false },
],
paranoid: false,
order: [[sort, direction]],
@@ -235,9 +235,9 @@ router.post('documents.deleted', auth(), pagination(), async ctx => {
};
});
router.post('documents.viewed', auth(), pagination(), async ctx => {
let { sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
router.post("documents.viewed", auth(), pagination(), async ctx => {
let { sort = "updatedAt", direction } = ctx.body;
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const collectionIds = await user.collectionIds();
@@ -255,7 +255,7 @@ router.post('documents.viewed', auth(), pagination(), async ctx => {
include: [
{
model: Star,
as: 'starred',
as: "starred",
where: { userId: user.id },
required: false,
},
@@ -280,9 +280,9 @@ router.post('documents.viewed', auth(), pagination(), async ctx => {
};
});
router.post('documents.starred', auth(), pagination(), async ctx => {
let { sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
router.post("documents.starred", auth(), pagination(), async ctx => {
let { sort = "updatedAt", direction } = ctx.body;
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const collectionIds = await user.collectionIds();
@@ -301,11 +301,11 @@ router.post('documents.starred', auth(), pagination(), async ctx => {
include: [
{
model: Collection,
as: 'collection',
as: "collection",
},
{
model: Star,
as: 'starred',
as: "starred",
where: {
userId: user.id,
},
@@ -331,16 +331,16 @@ router.post('documents.starred', auth(), pagination(), async ctx => {
};
});
router.post('documents.drafts', auth(), pagination(), async ctx => {
let { sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
router.post("documents.drafts", auth(), pagination(), async ctx => {
let { sort = "updatedAt", direction } = ctx.body;
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const collectionIds = await user.collectionIds();
const collectionScope = { method: ['withCollection', user.id] };
const collectionScope = { method: ["withCollection", user.id] };
const documents = await Document.scope(
'defaultScope',
"defaultScope",
collectionScope
).findAll({
where: {
@@ -366,9 +366,9 @@ router.post('documents.drafts', auth(), pagination(), async ctx => {
};
});
router.post('documents.info', auth({ required: false }), async ctx => {
router.post("documents.info", auth({ required: false }), async ctx => {
const { id, shareId } = ctx.body;
ctx.assertPresent(id || shareId, 'id or shareId is required');
ctx.assertPresent(id || shareId, "id or shareId is required");
const user = ctx.state.user;
let document;
@@ -384,16 +384,16 @@ router.post('documents.info', auth({ required: false }), async ctx => {
// unscoping here allows us to return unpublished documents
model: Document.unscoped(),
include: [
{ model: User, as: 'createdBy', paranoid: false },
{ model: User, as: 'updatedBy', paranoid: false },
{ model: User, as: "createdBy", paranoid: false },
{ model: User, as: "updatedBy", paranoid: false },
],
required: true,
as: 'document',
as: "document",
},
],
});
if (!share || share.document.archivedAt) {
throw new InvalidRequestError('Document could not be found for shareId');
throw new InvalidRequestError("Document could not be found for shareId");
}
document = share.document;
} else {
@@ -401,10 +401,10 @@ router.post('documents.info', auth({ required: false }), async ctx => {
id,
user ? { userId: user.id } : undefined
);
authorize(user, 'read', document);
authorize(user, "read", document);
}
const isPublic = cannot(user, 'read', document);
const isPublic = cannot(user, "read", document);
ctx.body = {
data: await presentDocument(document, { isPublic }),
@@ -412,9 +412,9 @@ router.post('documents.info', auth({ required: false }), async ctx => {
};
});
router.post('documents.restore', auth(), async ctx => {
router.post("documents.restore", auth(), async ctx => {
const { id, revisionId } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, {
@@ -423,13 +423,13 @@ router.post('documents.restore', auth(), async ctx => {
});
if (document.deletedAt) {
authorize(user, 'restore', document);
authorize(user, "restore", document);
// restore a previously deleted document
await document.unarchive(user.id);
await Event.create({
name: 'documents.restore',
name: "documents.restore",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -438,13 +438,13 @@ router.post('documents.restore', auth(), async ctx => {
ip: ctx.request.ip,
});
} else if (document.archivedAt) {
authorize(user, 'unarchive', document);
authorize(user, "unarchive", document);
// restore a previously archived document
await document.unarchive(user.id);
await Event.create({
name: 'documents.unarchive',
name: "documents.unarchive",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -454,17 +454,17 @@ router.post('documents.restore', auth(), async ctx => {
});
} else if (revisionId) {
// restore a document to a specific revision
authorize(user, 'update', document);
authorize(user, "update", document);
const revision = await Revision.findByPk(revisionId);
authorize(document, 'restore', revision);
authorize(document, "restore", revision);
document.text = revision.text;
document.title = revision.title;
await document.save();
await Event.create({
name: 'documents.restore',
name: "documents.restore",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -473,7 +473,7 @@ router.post('documents.restore', auth(), async ctx => {
ip: ctx.request.ip,
});
} else {
ctx.assertPresent(revisionId, 'revisionId is required');
ctx.assertPresent(revisionId, "revisionId is required");
}
ctx.body = {
@@ -482,7 +482,7 @@ router.post('documents.restore', auth(), async ctx => {
};
});
router.post('documents.search', auth(), pagination(), async ctx => {
router.post("documents.search", auth(), pagination(), async ctx => {
const {
query,
includeArchived,
@@ -493,34 +493,34 @@ router.post('documents.search', auth(), pagination(), async ctx => {
} = ctx.body;
const { offset, limit } = ctx.state.pagination;
const user = ctx.state.user;
ctx.assertPresent(query, 'query is required');
ctx.assertPresent(query, "query is required");
if (collectionId) {
ctx.assertUuid(collectionId, 'collectionId must be a UUID');
ctx.assertUuid(collectionId, "collectionId must be a UUID");
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findByPk(collectionId);
authorize(user, 'read', collection);
authorize(user, "read", collection);
}
let collaboratorIds = undefined;
if (userId) {
ctx.assertUuid(userId, 'userId must be a UUID');
ctx.assertUuid(userId, "userId must be a UUID");
collaboratorIds = [userId];
}
if (dateFilter) {
ctx.assertIn(
dateFilter,
['day', 'week', 'month', 'year'],
'dateFilter must be one of day,week,month,year'
["day", "week", "month", "year"],
"dateFilter must be one of day,week,month,year"
);
}
const results = await Document.searchForUser(user, query, {
includeArchived: includeArchived === 'true',
includeDrafts: includeDrafts === 'true',
includeArchived: includeArchived === "true",
includeDrafts: includeDrafts === "true",
collaboratorIds,
collectionId,
dateFilter,
@@ -545,19 +545,19 @@ router.post('documents.search', auth(), pagination(), async ctx => {
};
});
router.post('documents.pin', auth(), async ctx => {
router.post("documents.pin", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'pin', document);
authorize(user, "pin", document);
document.pinnedById = user.id;
await document.save();
await Event.create({
name: 'documents.pin',
name: "documents.pin",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -572,19 +572,19 @@ router.post('documents.pin', auth(), async ctx => {
};
});
router.post('documents.unpin', auth(), async ctx => {
router.post("documents.unpin", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'unpin', document);
authorize(user, "unpin", document);
document.pinnedById = null;
await document.save();
await Event.create({
name: 'documents.unpin',
name: "documents.unpin",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -599,20 +599,20 @@ router.post('documents.unpin', auth(), async ctx => {
};
});
router.post('documents.star', auth(), async ctx => {
router.post("documents.star", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'read', document);
authorize(user, "read", document);
await Star.findOrCreate({
where: { documentId: document.id, userId: user.id },
});
await Event.create({
name: 'documents.star',
name: "documents.star",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -626,20 +626,20 @@ router.post('documents.star', auth(), async ctx => {
};
});
router.post('documents.unstar', auth(), async ctx => {
router.post("documents.unstar", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'read', document);
authorize(user, "read", document);
await Star.destroy({
where: { documentId: document.id, userId: user.id },
});
await Event.create({
name: 'documents.unstar',
name: "documents.unstar",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -653,46 +653,46 @@ router.post('documents.unstar', auth(), async ctx => {
};
});
router.post('documents.create', auth(), async ctx => {
router.post("documents.create", auth(), async ctx => {
const {
title = '',
text = '',
title = "",
text = "",
publish,
collectionId,
parentDocumentId,
index,
} = ctx.body;
const editorVersion = ctx.headers['x-editor-version'];
const editorVersion = ctx.headers["x-editor-version"];
ctx.assertUuid(collectionId, 'collectionId must be an uuid');
ctx.assertUuid(collectionId, "collectionId must be an uuid");
if (parentDocumentId) {
ctx.assertUuid(parentDocumentId, 'parentDocumentId must be an uuid');
ctx.assertUuid(parentDocumentId, "parentDocumentId must be an uuid");
}
if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)');
if (index) ctx.assertPositiveInteger(index, "index must be an integer (>=0)");
const user = ctx.state.user;
authorize(user, 'create', Document);
authorize(user, "create", Document);
const collection = await Collection.scope({
method: ['withMembership', user.id],
method: ["withMembership", user.id],
}).findOne({
where: {
id: collectionId,
teamId: user.teamId,
},
});
authorize(user, 'publish', collection);
authorize(user, "publish", collection);
let parentDocument;
if (parentDocumentId && collection.type === 'atlas') {
if (parentDocumentId && collection.type === "atlas") {
parentDocument = await Document.findOne({
where: {
id: parentDocumentId,
collectionId: collection.id,
},
});
authorize(user, 'read', parentDocument, { collection });
authorize(user, "read", parentDocument, { collection });
}
let document = await Document.create({
@@ -708,7 +708,7 @@ router.post('documents.create', auth(), async ctx => {
});
await Event.create({
name: 'documents.create',
name: "documents.create",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -721,7 +721,7 @@ router.post('documents.create', auth(), async ctx => {
await document.publish();
await Event.create({
name: 'documents.publish',
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -745,7 +745,7 @@ router.post('documents.create', auth(), async ctx => {
};
});
router.post('documents.update', auth(), async ctx => {
router.post("documents.update", auth(), async ctx => {
const {
id,
title,
@@ -756,18 +756,18 @@ router.post('documents.update', auth(), async ctx => {
lastRevision,
append,
} = ctx.body;
const editorVersion = ctx.headers['x-editor-version'];
const editorVersion = ctx.headers["x-editor-version"];
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(title || text, 'title or text is required');
if (append) ctx.assertPresent(text, 'Text is required while appending');
ctx.assertPresent(id, "id is required");
ctx.assertPresent(title || text, "title or text is required");
if (append) ctx.assertPresent(text, "Text is required while appending");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'update', document);
authorize(user, "update", document);
if (lastRevision && lastRevision !== document.revisionCount) {
throw new InvalidRequestError('Document has changed since last revision');
throw new InvalidRequestError("Document has changed since last revision");
}
// Update document
@@ -801,7 +801,7 @@ router.post('documents.update', auth(), async ctx => {
if (publish) {
await Event.create({
name: 'documents.publish',
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -811,7 +811,7 @@ router.post('documents.update', auth(), async ctx => {
});
} else {
await Event.create({
name: 'documents.update',
name: "documents.update",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -834,31 +834,31 @@ router.post('documents.update', auth(), async ctx => {
};
});
router.post('documents.move', auth(), async ctx => {
router.post("documents.move", auth(), async ctx => {
const { id, collectionId, parentDocumentId, index } = ctx.body;
ctx.assertUuid(id, 'id must be a uuid');
ctx.assertUuid(collectionId, 'collectionId must be a uuid');
ctx.assertUuid(id, "id must be a uuid");
ctx.assertUuid(collectionId, "collectionId must be a uuid");
if (parentDocumentId) {
ctx.assertUuid(parentDocumentId, 'parentDocumentId must be a uuid');
ctx.assertUuid(parentDocumentId, "parentDocumentId must be a uuid");
}
if (index) {
ctx.assertPositiveInteger(index, 'index must be a positive integer');
ctx.assertPositiveInteger(index, "index must be a positive integer");
}
if (parentDocumentId === id) {
throw new InvalidRequestError(
'Infinite loop detected, cannot nest a document inside itself'
"Infinite loop detected, cannot nest a document inside itself"
);
}
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'move', document);
authorize(user, "move", document);
const { collection } = document;
if (collection.type !== 'atlas' && parentDocumentId) {
if (collection.type !== "atlas" && parentDocumentId) {
throw new InvalidRequestError(
'Document cannot be nested in this collection type'
"Document cannot be nested in this collection type"
);
}
@@ -866,7 +866,7 @@ router.post('documents.move', auth(), async ctx => {
const parent = await Document.findByPk(parentDocumentId, {
userId: user.id,
});
authorize(user, 'update', parent);
authorize(user, "update", parent);
}
const { documents, collections } = await documentMover({
@@ -891,18 +891,18 @@ router.post('documents.move', auth(), async ctx => {
};
});
router.post('documents.archive', auth(), async ctx => {
router.post("documents.archive", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'archive', document);
authorize(user, "archive", document);
await document.archive(user.id);
await Event.create({
name: 'documents.archive',
name: "documents.archive",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -917,18 +917,18 @@ router.post('documents.archive', auth(), async ctx => {
};
});
router.post('documents.delete', auth(), async ctx => {
router.post("documents.delete", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'delete', document);
authorize(user, "delete", document);
await document.delete();
await Event.create({
name: 'documents.delete',
name: "documents.delete",
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,19 @@
// @flow
import Sequelize from 'sequelize';
import Router from 'koa-router';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import { presentEvent } from '../presenters';
import { Event, Team, User } from '../models';
import policy from '../policies';
import Sequelize from "sequelize";
import Router from "koa-router";
import auth from "../middlewares/authentication";
import pagination from "./middlewares/pagination";
import { presentEvent } from "../presenters";
import { Event, Team, User } from "../models";
import policy from "../policies";
const Op = Sequelize.Op;
const { authorize } = policy;
const router = new Router();
router.post('events.list', auth(), pagination(), async ctx => {
let { sort = 'createdAt', direction, auditLog = false } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
router.post("events.list", auth(), pagination(), async ctx => {
let { sort = "createdAt", direction, auditLog = false } = ctx.body;
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const paranoid = false;
@@ -33,7 +33,7 @@ router.post('events.list', auth(), pagination(), async ctx => {
};
if (auditLog) {
authorize(user, 'auditLog', Team);
authorize(user, "auditLog", Team);
where.name = Event.AUDIT_EVENTS;
}
@@ -43,7 +43,7 @@ router.post('events.list', auth(), pagination(), async ctx => {
include: [
{
model: User,
as: 'actor',
as: "actor",
paranoid: false,
},
],

View File

@@ -1,21 +1,21 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { flushdb, seed } from '../test/support';
import { buildEvent } from '../test/factories';
import TestServer from "fetch-test-server";
import app from "../app";
import { flushdb, seed } from "../test/support";
import { buildEvent } from "../test/factories";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#events.list', async () => {
it('should only return activity events', async () => {
describe("#events.list", async () => {
it("should only return activity events", async () => {
const { user, admin, document, collection } = await seed();
// private event
await buildEvent({
name: 'users.promote',
name: "users.promote",
teamId: user.teamId,
actorId: admin.id,
userId: user.id,
@@ -23,13 +23,13 @@ describe('#events.list', async () => {
// event viewable in activity stream
const event = await buildEvent({
name: 'documents.publish',
name: "documents.publish",
collectionId: collection.id,
documentId: document.id,
teamId: user.teamId,
actorId: admin.id,
});
const res = await server.post('/api/events.list', {
const res = await server.post("/api/events.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
@@ -39,12 +39,12 @@ describe('#events.list', async () => {
expect(body.data[0].id).toEqual(event.id);
});
it('should return events with deleted actors', async () => {
it("should return events with deleted actors", async () => {
const { user, admin, document, collection } = await seed();
// event viewable in activity stream
const event = await buildEvent({
name: 'documents.publish',
name: "documents.publish",
collectionId: collection.id,
documentId: document.id,
teamId: user.teamId,
@@ -53,7 +53,7 @@ describe('#events.list', async () => {
await user.destroy();
const res = await server.post('/api/events.list', {
const res = await server.post("/api/events.list", {
body: { token: admin.getJwtToken() },
});
@@ -64,8 +64,8 @@ describe('#events.list', async () => {
expect(body.data[0].id).toEqual(event.id);
});
it('should require authentication', async () => {
const res = await server.post('/api/events.list');
it("should require authentication", async () => {
const res = await server.post("/api/events.list");
const body = await res.json();
expect(res.status).toEqual(401);

View File

@@ -1,26 +1,26 @@
// @flow
import Router from 'koa-router';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import { Op } from '../sequelize';
import { MAX_AVATAR_DISPLAY } from '../../shared/constants';
import Router from "koa-router";
import auth from "../middlewares/authentication";
import pagination from "./middlewares/pagination";
import { Op } from "../sequelize";
import { MAX_AVATAR_DISPLAY } from "../../shared/constants";
import {
presentGroup,
presentPolicies,
presentUser,
presentGroupMembership,
} from '../presenters';
import { User, Event, Group, GroupUser } from '../models';
import policy from '../policies';
} from "../presenters";
import { User, Event, Group, GroupUser } from "../models";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('groups.list', auth(), pagination(), async ctx => {
const { sort = 'updatedAt' } = ctx.body;
router.post("groups.list", auth(), pagination(), async ctx => {
const { sort = "updatedAt" } = ctx.body;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
let groups = await Group.findAll({
@@ -55,13 +55,13 @@ router.post('groups.list', auth(), pagination(), async ctx => {
};
});
router.post('groups.info', auth(), async ctx => {
router.post("groups.info", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const group = await Group.findByPk(id);
authorize(user, 'read', group);
authorize(user, "read", group);
ctx.body = {
data: presentGroup(group),
@@ -69,13 +69,13 @@ router.post('groups.info', auth(), async ctx => {
};
});
router.post('groups.create', auth(), async ctx => {
router.post("groups.create", auth(), async ctx => {
const { name } = ctx.body;
ctx.assertPresent(name, 'name is required');
ctx.assertPresent(name, "name is required");
const user = ctx.state.user;
authorize(user, 'create', Group);
authorize(user, "create", Group);
let group = await Group.create({
name,
teamId: user.teamId,
@@ -86,7 +86,7 @@ router.post('groups.create', auth(), async ctx => {
group = await Group.findByPk(group.id);
await Event.create({
name: 'groups.create',
name: "groups.create",
actorId: user.id,
teamId: user.teamId,
modelId: group.id,
@@ -100,22 +100,22 @@ router.post('groups.create', auth(), async ctx => {
};
});
router.post('groups.update', auth(), async ctx => {
router.post("groups.update", auth(), async ctx => {
const { id, name } = ctx.body;
ctx.assertPresent(name, 'name is required');
ctx.assertUuid(id, 'id is required');
ctx.assertPresent(name, "name is required");
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const group = await Group.findByPk(id);
authorize(user, 'update', group);
authorize(user, "update", group);
group.name = name;
if (group.changed()) {
await group.save();
await Event.create({
name: 'groups.update',
name: "groups.update",
teamId: user.teamId,
actorId: user.id,
modelId: group.id,
@@ -130,18 +130,18 @@ router.post('groups.update', auth(), async ctx => {
};
});
router.post('groups.delete', auth(), async ctx => {
router.post("groups.delete", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const { user } = ctx.state;
const group = await Group.findByPk(id);
authorize(user, 'delete', group);
authorize(user, "delete", group);
await group.destroy();
await Event.create({
name: 'groups.delete',
name: "groups.delete",
actorId: user.id,
modelId: group.id,
teamId: group.teamId,
@@ -154,13 +154,13 @@ router.post('groups.delete', auth(), async ctx => {
};
});
router.post('groups.memberships', auth(), pagination(), async ctx => {
router.post("groups.memberships", auth(), pagination(), async ctx => {
const { id, query } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const group = await Group.findByPk(id);
authorize(user, 'read', group);
authorize(user, "read", group);
let userWhere;
if (query) {
@@ -173,13 +173,13 @@ router.post('groups.memberships', auth(), pagination(), async ctx => {
const memberships = await GroupUser.findAll({
where: { groupId: id },
order: [['createdAt', 'DESC']],
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
include: [
{
model: User,
as: 'user',
as: "user",
where: userWhere,
required: true,
},
@@ -195,16 +195,16 @@ router.post('groups.memberships', auth(), pagination(), async ctx => {
};
});
router.post('groups.add_user', auth(), async ctx => {
router.post("groups.add_user", auth(), async ctx => {
const { id, userId } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(userId, 'userId is required');
ctx.assertUuid(id, "id is required");
ctx.assertUuid(userId, "userId is required");
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'read', user);
authorize(ctx.state.user, "read", user);
let group = await Group.findByPk(id);
authorize(ctx.state.user, 'update', group);
authorize(ctx.state.user, "update", group);
let membership = await GroupUser.findOne({
where: {
@@ -230,7 +230,7 @@ router.post('groups.add_user', auth(), async ctx => {
group = await Group.findByPk(id);
await Event.create({
name: 'groups.add_user',
name: "groups.add_user",
userId,
teamId: user.teamId,
modelId: group.id,
@@ -249,21 +249,21 @@ router.post('groups.add_user', auth(), async ctx => {
};
});
router.post('groups.remove_user', auth(), async ctx => {
router.post("groups.remove_user", auth(), async ctx => {
const { id, userId } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(userId, 'userId is required');
ctx.assertUuid(id, "id is required");
ctx.assertUuid(userId, "userId is required");
let group = await Group.findByPk(id);
authorize(ctx.state.user, 'update', group);
authorize(ctx.state.user, "update", group);
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'read', user);
authorize(ctx.state.user, "read", user);
await group.removeUser(user);
await Event.create({
name: 'groups.remove_user',
name: "groups.remove_user",
userId,
modelId: group.id,
teamId: user.teamId,

View File

@@ -1,21 +1,21 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { flushdb } from '../test/support';
import { buildUser, buildGroup } from '../test/factories';
import { Event } from '../models';
import TestServer from "fetch-test-server";
import app from "../app";
import { flushdb } from "../test/support";
import { buildUser, buildGroup } from "../test/factories";
import { Event } from "../models";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#groups.create', async () => {
it('should create a group', async () => {
const name = 'hello I am a group';
describe("#groups.create", async () => {
it("should create a group", async () => {
const name = "hello I am a group";
const user = await buildUser({ isAdmin: true });
const res = await server.post('/api/groups.create', {
const res = await server.post("/api/groups.create", {
body: { token: user.getJwtToken(), name },
});
@@ -26,11 +26,11 @@ describe('#groups.create', async () => {
});
});
describe('#groups.update', async () => {
it('should require authentication', async () => {
describe("#groups.update", async () => {
it("should require authentication", async () => {
const group = await buildGroup();
const res = await server.post('/api/groups.update', {
body: { id: group.id, name: 'Test' },
const res = await server.post("/api/groups.update", {
body: { id: group.id, name: "Test" },
});
const body = await res.json();
@@ -38,26 +38,26 @@ describe('#groups.update', async () => {
expect(body).toMatchSnapshot();
});
it('should require admin', async () => {
it("should require admin", async () => {
const group = await buildGroup();
const user = await buildUser();
const res = await server.post('/api/groups.update', {
body: { token: user.getJwtToken(), id: group.id, name: 'Test' },
const res = await server.post("/api/groups.update", {
body: { token: user.getJwtToken(), id: group.id, name: "Test" },
});
expect(res.status).toEqual(403);
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const group = await buildGroup();
const user = await buildUser({ isAdmin: true });
const res = await server.post('/api/groups.update', {
body: { token: user.getJwtToken(), id: group.id, name: 'Test' },
const res = await server.post("/api/groups.update", {
body: { token: user.getJwtToken(), id: group.id, name: "Test" },
});
expect(res.status).toEqual(403);
});
describe('when user is admin', async () => {
describe("when user is admin", async () => {
let user, group;
beforeEach(async () => {
@@ -65,9 +65,9 @@ describe('#groups.update', async () => {
group = await buildGroup({ teamId: user.teamId });
});
it('allows admin to edit a group', async () => {
const res = await server.post('/api/groups.update', {
body: { token: user.getJwtToken(), id: group.id, name: 'Test' },
it("allows admin to edit a group", async () => {
const res = await server.post("/api/groups.update", {
body: { token: user.getJwtToken(), id: group.id, name: "Test" },
});
const events = await Event.findAll();
@@ -75,11 +75,11 @@ describe('#groups.update', async () => {
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.name).toBe('Test');
expect(body.data.name).toBe("Test");
});
it('does not create an event if the update is a noop', async () => {
const res = await server.post('/api/groups.update', {
it("does not create an event if the update is a noop", async () => {
const res = await server.post("/api/groups.update", {
body: { token: user.getJwtToken(), id: group.id, name: group.name },
});
@@ -91,17 +91,17 @@ describe('#groups.update', async () => {
expect(body.data.name).toBe(group.name);
});
it('fails with validation error when name already taken', async () => {
it("fails with validation error when name already taken", async () => {
await buildGroup({
teamId: user.teamId,
name: 'test',
name: "test",
});
const res = await server.post('/api/groups.update', {
const res = await server.post("/api/groups.update", {
body: {
token: user.getJwtToken(),
id: group.id,
name: 'TEST',
name: "TEST",
},
});
@@ -112,40 +112,40 @@ describe('#groups.update', async () => {
});
});
describe('#groups.list', async () => {
it('should require authentication', async () => {
const res = await server.post('/api/groups.list');
describe("#groups.list", async () => {
it("should require authentication", async () => {
const res = await server.post("/api/groups.list");
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should return groups with memberships preloaded', async () => {
it("should return groups with memberships preloaded", async () => {
const user = await buildUser();
const group = await buildGroup({ teamId: user.teamId });
await group.addUser(user, { through: { createdById: user.id } });
const res = await server.post('/api/groups.list', {
const res = await server.post("/api/groups.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data['groups'].length).toEqual(1);
expect(body.data['groups'][0].id).toEqual(group.id);
expect(body.data["groups"].length).toEqual(1);
expect(body.data["groups"][0].id).toEqual(group.id);
expect(body.data['groupMemberships'].length).toEqual(1);
expect(body.data['groupMemberships'][0].groupId).toEqual(group.id);
expect(body.data['groupMemberships'][0].user.id).toEqual(user.id);
expect(body.data["groupMemberships"].length).toEqual(1);
expect(body.data["groupMemberships"][0].groupId).toEqual(group.id);
expect(body.data["groupMemberships"][0].user.id).toEqual(user.id);
expect(body.policies.length).toEqual(1);
expect(body.policies[0].abilities.read).toEqual(true);
});
it('should return groups when membership user is deleted', async () => {
it("should return groups when membership user is deleted", async () => {
const me = await buildUser();
const user = await buildUser({ teamId: me.teamId });
const group = await buildGroup({ teamId: user.teamId });
@@ -154,31 +154,31 @@ describe('#groups.list', async () => {
await group.addUser(me, { through: { createdById: me.id } });
await user.destroy();
const res = await server.post('/api/groups.list', {
const res = await server.post("/api/groups.list", {
body: { token: me.getJwtToken() },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data['groups'].length).toEqual(1);
expect(body.data['groups'][0].id).toEqual(group.id);
expect(body.data["groups"].length).toEqual(1);
expect(body.data["groups"][0].id).toEqual(group.id);
expect(body.data['groupMemberships'].length).toEqual(1);
expect(body.data['groupMemberships'][0].groupId).toEqual(group.id);
expect(body.data['groupMemberships'][0].user.id).toEqual(me.id);
expect(body.data["groupMemberships"].length).toEqual(1);
expect(body.data["groupMemberships"][0].groupId).toEqual(group.id);
expect(body.data["groupMemberships"][0].user.id).toEqual(me.id);
expect(body.policies.length).toEqual(1);
expect(body.policies[0].abilities.read).toEqual(true);
});
});
describe('#groups.info', async () => {
it('should return group if admin', async () => {
describe("#groups.info", async () => {
it("should return group if admin", async () => {
const user = await buildUser({ isAdmin: true });
const group = await buildGroup({ teamId: user.teamId });
const res = await server.post('/api/groups.info', {
const res = await server.post("/api/groups.info", {
body: { token: user.getJwtToken(), id: group.id },
});
@@ -188,12 +188,12 @@ describe('#groups.info', async () => {
expect(body.data.id).toEqual(group.id);
});
it('should return group if member', async () => {
it("should return group if member", async () => {
const user = await buildUser();
const group = await buildGroup({ teamId: user.teamId });
await group.addUser(user, { through: { createdById: user.id } });
const res = await server.post('/api/groups.info', {
const res = await server.post("/api/groups.info", {
body: { token: user.getJwtToken(), id: group.id },
});
@@ -203,39 +203,39 @@ describe('#groups.info', async () => {
expect(body.data.id).toEqual(group.id);
});
it('should not return group if non-member, non-admin', async () => {
it("should not return group if non-member, non-admin", async () => {
const user = await buildUser();
const group = await buildGroup({ teamId: user.teamId });
const res = await server.post('/api/groups.info', {
const res = await server.post("/api/groups.info", {
body: { token: user.getJwtToken(), id: group.id },
});
expect(res.status).toEqual(403);
});
it('should require authentication', async () => {
const res = await server.post('/api/groups.info');
it("should require authentication", async () => {
const res = await server.post("/api/groups.info");
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const user = await buildUser();
const group = await buildGroup();
const res = await server.post('/api/groups.info', {
const res = await server.post("/api/groups.info", {
body: { token: user.getJwtToken(), id: group.id },
});
expect(res.status).toEqual(403);
});
});
describe('#groups.delete', async () => {
it('should require authentication', async () => {
describe("#groups.delete", async () => {
it("should require authentication", async () => {
const group = await buildGroup();
const res = await server.post('/api/groups.delete', {
const res = await server.post("/api/groups.delete", {
body: { id: group.id },
});
const body = await res.json();
@@ -244,30 +244,30 @@ describe('#groups.delete', async () => {
expect(body).toMatchSnapshot();
});
it('should require admin', async () => {
it("should require admin", async () => {
const group = await buildGroup();
const user = await buildUser();
const res = await server.post('/api/groups.delete', {
const res = await server.post("/api/groups.delete", {
body: { token: user.getJwtToken(), id: group.id },
});
expect(res.status).toEqual(403);
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const group = await buildGroup();
const user = await buildUser({ isAdmin: true });
const res = await server.post('/api/groups.delete', {
const res = await server.post("/api/groups.delete", {
body: { token: user.getJwtToken(), id: group.id },
});
expect(res.status).toEqual(403);
});
it('allows admin to delete a group', async () => {
it("allows admin to delete a group", async () => {
const user = await buildUser({ isAdmin: true });
const group = await buildGroup({ teamId: user.teamId });
const res = await server.post('/api/groups.delete', {
const res = await server.post("/api/groups.delete", {
body: { token: user.getJwtToken(), id: group.id },
});
@@ -277,14 +277,14 @@ describe('#groups.delete', async () => {
});
});
describe('#groups.memberships', async () => {
it('should return members in a group', async () => {
describe("#groups.memberships", async () => {
it("should return members in a group", async () => {
const user = await buildUser();
const group = await buildGroup({ teamId: user.teamId });
await group.addUser(user, { through: { createdById: user.id } });
const res = await server.post('/api/groups.memberships', {
const res = await server.post("/api/groups.memberships", {
body: { token: user.getJwtToken(), id: group.id },
});
@@ -297,10 +297,10 @@ describe('#groups.memberships', async () => {
expect(body.data.groupMemberships[0].user.id).toEqual(user.id);
});
it('should allow filtering members in group by name', async () => {
it("should allow filtering members in group by name", async () => {
const user = await buildUser();
const user2 = await buildUser({ name: "Won't find" });
const user3 = await buildUser({ teamId: user.teamId, name: 'Deleted' });
const user3 = await buildUser({ teamId: user.teamId, name: "Deleted" });
const group = await buildGroup({ teamId: user.teamId });
await group.addUser(user, { through: { createdById: user.id } });
@@ -309,7 +309,7 @@ describe('#groups.memberships', async () => {
await user3.destroy();
const res = await server.post('/api/groups.memberships', {
const res = await server.post("/api/groups.memberships", {
body: {
token: user.getJwtToken(),
id: group.id,
@@ -323,33 +323,33 @@ describe('#groups.memberships', async () => {
expect(body.data.users[0].id).toEqual(user.id);
});
it('should require authentication', async () => {
const res = await server.post('/api/groups.memberships');
it("should require authentication", async () => {
const res = await server.post("/api/groups.memberships");
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const user = await buildUser();
const group = await buildGroup();
const res = await server.post('/api/groups.memberships', {
const res = await server.post("/api/groups.memberships", {
body: { token: user.getJwtToken(), id: group.id },
});
expect(res.status).toEqual(403);
});
});
describe('#groups.add_user', async () => {
it('should add user to group', async () => {
describe("#groups.add_user", async () => {
it("should add user to group", async () => {
const user = await buildUser({ isAdmin: true });
const group = await buildGroup({
teamId: user.teamId,
});
const res = await server.post('/api/groups.add_user', {
const res = await server.post("/api/groups.add_user", {
body: {
token: user.getJwtToken(),
id: group.id,
@@ -362,19 +362,19 @@ describe('#groups.add_user', async () => {
expect(users.length).toEqual(1);
});
it('should require authentication', async () => {
const res = await server.post('/api/groups.add_user');
it("should require authentication", async () => {
const res = await server.post("/api/groups.add_user");
expect(res.status).toEqual(401);
});
it('should require user in team', async () => {
it("should require user in team", async () => {
const user = await buildUser({ isAdmin: true });
const group = await buildGroup({
teamId: user.teamId,
});
const anotherUser = await buildUser();
const res = await server.post('/api/groups.add_user', {
const res = await server.post("/api/groups.add_user", {
body: {
token: user.getJwtToken(),
id: group.id,
@@ -388,14 +388,14 @@ describe('#groups.add_user', async () => {
expect(body).toMatchSnapshot();
});
it('should require admin', async () => {
it("should require admin", async () => {
const user = await buildUser();
const group = await buildGroup({
teamId: user.teamId,
});
const anotherUser = await buildUser({ teamId: user.teamId });
const res = await server.post('/api/groups.add_user', {
const res = await server.post("/api/groups.add_user", {
body: {
token: user.getJwtToken(),
id: group.id,
@@ -410,14 +410,14 @@ describe('#groups.add_user', async () => {
});
});
describe('#groups.remove_user', async () => {
it('should remove user from group', async () => {
describe("#groups.remove_user", async () => {
it("should remove user from group", async () => {
const user = await buildUser({ isAdmin: true });
const group = await buildGroup({
teamId: user.teamId,
});
await server.post('/api/groups.add_user', {
await server.post("/api/groups.add_user", {
body: {
token: user.getJwtToken(),
id: group.id,
@@ -428,7 +428,7 @@ describe('#groups.remove_user', async () => {
const users = await group.getUsers();
expect(users.length).toEqual(1);
const res = await server.post('/api/groups.remove_user', {
const res = await server.post("/api/groups.remove_user", {
body: {
token: user.getJwtToken(),
id: group.id,
@@ -441,20 +441,20 @@ describe('#groups.remove_user', async () => {
expect(users1.length).toEqual(0);
});
it('should require authentication', async () => {
const res = await server.post('/api/groups.remove_user');
it("should require authentication", async () => {
const res = await server.post("/api/groups.remove_user");
expect(res.status).toEqual(401);
});
it('should require user in team', async () => {
it("should require user in team", async () => {
const user = await buildUser({ isAdmin: true });
const group = await buildGroup({
teamId: user.teamId,
});
const anotherUser = await buildUser();
const res = await server.post('/api/groups.remove_user', {
const res = await server.post("/api/groups.remove_user", {
body: {
token: user.getJwtToken(),
id: group.id,
@@ -467,7 +467,7 @@ describe('#groups.remove_user', async () => {
expect(body).toMatchSnapshot();
});
it('should require admin', async () => {
it("should require admin", async () => {
const user = await buildUser();
const group = await buildGroup({
teamId: user.teamId,
@@ -476,7 +476,7 @@ describe('#groups.remove_user', async () => {
teamId: user.teamId,
});
const res = await server.post('/api/groups.remove_user', {
const res = await server.post("/api/groups.remove_user", {
body: {
token: user.getJwtToken(),
id: group.id,

View File

@@ -1,35 +1,35 @@
// @flow
import Router from 'koa-router';
import { escapeRegExp } from 'lodash';
import { AuthenticationError, InvalidRequestError } from '../errors';
import { Authentication, Document, User, Team, Collection } from '../models';
import { presentSlackAttachment } from '../presenters';
import * as Slack from '../slack';
import Router from "koa-router";
import { escapeRegExp } from "lodash";
import { AuthenticationError, InvalidRequestError } from "../errors";
import { Authentication, Document, User, Team, Collection } from "../models";
import { presentSlackAttachment } from "../presenters";
import * as Slack from "../slack";
const router = new Router();
// triggered by a user posting a getoutline.com link in Slack
router.post('hooks.unfurl', async ctx => {
router.post("hooks.unfurl", async ctx => {
const { challenge, token, event } = ctx.body;
if (challenge) return (ctx.body = ctx.body.challenge);
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
throw new AuthenticationError('Invalid token');
throw new AuthenticationError("Invalid token");
}
const user = await User.findOne({
where: { service: 'slack', serviceId: event.user },
where: { service: "slack", serviceId: event.user },
});
if (!user) return;
const auth = await Authentication.findOne({
where: { service: 'slack', teamId: user.teamId },
where: { service: "slack", teamId: user.teamId },
});
if (!auth) return;
// get content for unfurled links
let unfurls = {};
for (let link of event.links) {
const id = link.url.substr(link.url.lastIndexOf('/') + 1);
const id = link.url.substr(link.url.lastIndexOf("/") + 1);
const doc = await Document.findByPk(id);
if (!doc || doc.teamId !== user.teamId) continue;
@@ -40,7 +40,7 @@ router.post('hooks.unfurl', async ctx => {
};
}
await Slack.post('chat.unfurl', {
await Slack.post("chat.unfurl", {
token: auth.token,
channel: event.channel,
ts: event.message_ts,
@@ -49,17 +49,17 @@ router.post('hooks.unfurl', async ctx => {
});
// triggered by interactions with actions, dialogs, message buttons in Slack
router.post('hooks.interactive', async ctx => {
router.post("hooks.interactive", async ctx => {
const { payload } = ctx.body;
ctx.assertPresent(payload, 'payload is required');
ctx.assertPresent(payload, "payload is required");
const data = JSON.parse(payload);
const { callback_id, token } = data;
ctx.assertPresent(token, 'token is required');
ctx.assertPresent(callback_id, 'callback_id is required');
ctx.assertPresent(token, "token is required");
ctx.assertPresent(callback_id, "callback_id is required");
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
throw new AuthenticationError('Invalid verification token');
throw new AuthenticationError("Invalid verification token");
}
const team = await Team.findOne({
@@ -69,8 +69,8 @@ router.post('hooks.interactive', async ctx => {
if (!team) {
ctx.body = {
text:
'Sorry, we couldnt find an integration for your team. Head to your Outline settings to set one up.',
response_type: 'ephemeral',
"Sorry, we couldnt find an integration for your team. Head to your Outline settings to set one up.",
response_type: "ephemeral",
replace_original: false,
};
return;
@@ -83,13 +83,13 @@ router.post('hooks.interactive', async ctx => {
teamId: team.id,
},
});
if (!document) throw new InvalidRequestError('Invalid document');
if (!document) throw new InvalidRequestError("Invalid document");
const collection = await Collection.findByPk(document.collectionId);
// respond with a public message that will be posted in the original channel
ctx.body = {
response_type: 'in_channel',
response_type: "in_channel",
replace_original: false,
attachments: [
presentSlackAttachment(document, collection, team, document.getSummary()),
@@ -98,25 +98,25 @@ router.post('hooks.interactive', async ctx => {
});
// triggered by the /outline command in Slack
router.post('hooks.slack', async ctx => {
const { token, team_id, user_id, text = '' } = ctx.body;
ctx.assertPresent(token, 'token is required');
ctx.assertPresent(team_id, 'team_id is required');
ctx.assertPresent(user_id, 'user_id is required');
router.post("hooks.slack", async ctx => {
const { token, team_id, user_id, text = "" } = ctx.body;
ctx.assertPresent(token, "token is required");
ctx.assertPresent(team_id, "team_id is required");
ctx.assertPresent(user_id, "user_id is required");
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
throw new AuthenticationError('Invalid verification token');
throw new AuthenticationError("Invalid verification token");
}
// Handle "help" command or no input
if (text.trim() === 'help' || !text.trim()) {
if (text.trim() === "help" || !text.trim()) {
ctx.body = {
response_type: 'ephemeral',
text: 'How to use /outline',
response_type: "ephemeral",
text: "How to use /outline",
attachments: [
{
text:
'To search your knowledgebase use `/outline keyword`. \nYouve already learned how to get help with `/outline help`.',
"To search your knowledgebase use `/outline keyword`. \nYouve already learned how to get help with `/outline help`.",
},
],
};
@@ -128,9 +128,9 @@ router.post('hooks.slack', async ctx => {
});
if (!team) {
ctx.body = {
response_type: 'ephemeral',
response_type: "ephemeral",
text:
'Sorry, we couldnt find an integration for your team. Head to your Outline settings to set one up.',
"Sorry, we couldnt find an integration for your team. Head to your Outline settings to set one up.",
};
return;
}
@@ -138,7 +138,7 @@ router.post('hooks.slack', async ctx => {
const user = await User.findOne({
where: {
teamId: team.id,
service: 'slack',
service: "slack",
serviceId: user_id,
},
});
@@ -166,9 +166,9 @@ router.post('hooks.slack', async ctx => {
process.env.SLACK_MESSAGE_ACTIONS
? [
{
name: 'post',
text: 'Post to Channel',
type: 'button',
name: "post",
text: "Post to Channel",
type: "button",
value: result.document.id,
},
]

View File

@@ -1,43 +1,43 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { Authentication } from '../models';
import { flushdb, seed } from '../test/support';
import { buildDocument } from '../test/factories';
import * as Slack from '../slack';
import TestServer from "fetch-test-server";
import app from "../app";
import { Authentication } from "../models";
import { flushdb, seed } from "../test/support";
import { buildDocument } from "../test/factories";
import * as Slack from "../slack";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
jest.mock('../slack', () => ({
jest.mock("../slack", () => ({
post: jest.fn(),
}));
describe('#hooks.unfurl', async () => {
it('should return documents', async () => {
describe("#hooks.unfurl", async () => {
it("should return documents", async () => {
const { user, document } = await seed();
await Authentication.create({
service: 'slack',
service: "slack",
userId: user.id,
teamId: user.teamId,
token: '',
token: "",
});
const res = await server.post('/api/hooks.unfurl', {
const res = await server.post("/api/hooks.unfurl", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
team_id: 'TXXXXXXXX',
api_app_id: 'AXXXXXXXXX',
team_id: "TXXXXXXXX",
api_app_id: "AXXXXXXXXX",
event: {
type: 'link_shared',
channel: 'Cxxxxxx',
type: "link_shared",
channel: "Cxxxxxx",
user: user.serviceId,
message_ts: '123456789.9875',
message_ts: "123456789.9875",
links: [
{
domain: 'getoutline.com',
domain: "getoutline.com",
url: document.url,
},
],
@@ -49,16 +49,16 @@ describe('#hooks.unfurl', async () => {
});
});
describe('#hooks.slack', async () => {
it('should return no matches', async () => {
describe("#hooks.slack", async () => {
it("should return no matches", async () => {
const { user, team } = await seed();
const res = await server.post('/api/hooks.slack', {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
text: 'dsfkndfskndsfkn',
text: "dsfkndfskndsfkn",
},
});
const body = await res.json();
@@ -66,19 +66,19 @@ describe('#hooks.slack', async () => {
expect(body.attachments).toEqual(undefined);
});
it('should return search results with summary if query is in title', async () => {
it("should return search results with summary if query is in title", async () => {
const { user, team } = await seed();
const document = await buildDocument({
title: 'This title contains a search term',
title: "This title contains a search term",
userId: user.id,
teamId: user.teamId,
});
const res = await server.post('/api/hooks.slack', {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
text: 'contains',
text: "contains",
},
});
const body = await res.json();
@@ -88,19 +88,19 @@ describe('#hooks.slack', async () => {
expect(body.attachments[0].text).toEqual(document.getSummary());
});
it('should return search results if query is regex-like', async () => {
it("should return search results if query is regex-like", async () => {
const { user, team } = await seed();
await buildDocument({
title: 'This title contains a search term',
title: "This title contains a search term",
userId: user.id,
teamId: user.teamId,
});
const res = await server.post('/api/hooks.slack', {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
text: '*contains',
text: "*contains",
},
});
const body = await res.json();
@@ -108,19 +108,19 @@ describe('#hooks.slack', async () => {
expect(body.attachments.length).toEqual(1);
});
it('should return search results with snippet if query is in text', async () => {
it("should return search results with snippet if query is in text", async () => {
const { user, team } = await seed();
const document = await buildDocument({
text: 'This title contains a search term',
text: "This title contains a search term",
userId: user.id,
teamId: user.teamId,
});
const res = await server.post('/api/hooks.slack', {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
text: 'contains',
text: "contains",
},
});
const body = await res.json();
@@ -128,62 +128,62 @@ describe('#hooks.slack', async () => {
expect(body.attachments.length).toEqual(1);
expect(body.attachments[0].title).toEqual(document.title);
expect(body.attachments[0].text).toEqual(
'This title *contains* a search term'
"This title *contains* a search term"
);
});
it('should respond with help content for help keyword', async () => {
it("should respond with help content for help keyword", async () => {
const { user, team } = await seed();
const res = await server.post('/api/hooks.slack', {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
text: 'help',
text: "help",
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.text.includes('How to use')).toEqual(true);
expect(body.text.includes("How to use")).toEqual(true);
});
it('should respond with help content for no keyword', async () => {
it("should respond with help content for no keyword", async () => {
const { user, team } = await seed();
const res = await server.post('/api/hooks.slack', {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
text: '',
text: "",
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.text.includes('How to use')).toEqual(true);
expect(body.text.includes("How to use")).toEqual(true);
});
it('should return search results with snippet for unknown user', async () => {
it("should return search results with snippet for unknown user", async () => {
const { user, team } = await seed();
// unpublished document will not be returned
await buildDocument({
text: 'This title contains a search term',
text: "This title contains a search term",
userId: user.id,
teamId: user.teamId,
publishedAt: null,
});
const document = await buildDocument({
text: 'This title contains a search term',
text: "This title contains a search term",
userId: user.id,
teamId: user.teamId,
});
const res = await server.post('/api/hooks.slack', {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: 'unknown-slack-user-id',
user_id: "unknown-slack-user-id",
team_id: team.slackId,
text: 'contains',
text: "contains",
},
});
const body = await res.json();
@@ -191,30 +191,30 @@ describe('#hooks.slack', async () => {
expect(body.attachments.length).toEqual(1);
expect(body.attachments[0].title).toEqual(document.title);
expect(body.attachments[0].text).toEqual(
'This title *contains* a search term'
"This title *contains* a search term"
);
});
it('should error if incorrect verification token', async () => {
it("should error if incorrect verification token", async () => {
const { user, team } = await seed();
const res = await server.post('/api/hooks.slack', {
const res = await server.post("/api/hooks.slack", {
body: {
token: 'wrong-verification-token',
token: "wrong-verification-token",
team_id: team.slackId,
user_id: user.serviceId,
text: 'Welcome',
text: "Welcome",
},
});
expect(res.status).toEqual(401);
});
});
describe('#hooks.interactive', async () => {
it('should respond with replacement message', async () => {
describe("#hooks.interactive", async () => {
it("should respond with replacement message", async () => {
const { user, team } = await seed();
const document = await buildDocument({
title: 'This title contains a search term',
title: "This title contains a search term",
userId: user.id,
teamId: user.teamId,
});
@@ -225,48 +225,48 @@ describe('#hooks.interactive', async () => {
team: { id: team.slackId },
callback_id: document.id,
});
const res = await server.post('/api/hooks.interactive', {
const res = await server.post("/api/hooks.interactive", {
body: { payload },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.response_type).toEqual('in_channel');
expect(body.response_type).toEqual("in_channel");
expect(body.attachments.length).toEqual(1);
expect(body.attachments[0].title).toEqual(document.title);
});
it('should respond with replacement message if unknown user', async () => {
it("should respond with replacement message if unknown user", async () => {
const { user, team } = await seed();
const document = await buildDocument({
title: 'This title contains a search term',
title: "This title contains a search term",
userId: user.id,
teamId: user.teamId,
});
const payload = JSON.stringify({
token: process.env.SLACK_VERIFICATION_TOKEN,
user: { id: 'unknown-slack-user-id' },
user: { id: "unknown-slack-user-id" },
team: { id: team.slackId },
callback_id: document.id,
});
const res = await server.post('/api/hooks.interactive', {
const res = await server.post("/api/hooks.interactive", {
body: { payload },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.response_type).toEqual('in_channel');
expect(body.response_type).toEqual("in_channel");
expect(body.attachments.length).toEqual(1);
expect(body.attachments[0].title).toEqual(document.title);
});
it('should error if incorrect verification token', async () => {
it("should error if incorrect verification token", async () => {
const { user } = await seed();
const payload = JSON.stringify({
token: 'wrong-verification-token',
token: "wrong-verification-token",
user: { id: user.serviceId, name: user.name },
callback_id: 'doesnt-matter',
callback_id: "doesnt-matter",
});
const res = await server.post('/api/hooks.interactive', {
const res = await server.post("/api/hooks.interactive", {
body: { payload },
});
expect(res.status).toEqual(401);

View File

@@ -1,32 +1,32 @@
// @flow
import bodyParser from 'koa-bodyparser';
import Koa from 'koa';
import Router from 'koa-router';
import bodyParser from "koa-bodyparser";
import Koa from "koa";
import Router from "koa-router";
import auth from './auth';
import events from './events';
import users from './users';
import collections from './collections';
import documents from './documents';
import revisions from './revisions';
import views from './views';
import hooks from './hooks';
import apiKeys from './apiKeys';
import shares from './shares';
import groups from './groups';
import team from './team';
import integrations from './integrations';
import notificationSettings from './notificationSettings';
import utils from './utils';
import attachments from './attachments';
import auth from "./auth";
import events from "./events";
import users from "./users";
import collections from "./collections";
import documents from "./documents";
import revisions from "./revisions";
import views from "./views";
import hooks from "./hooks";
import apiKeys from "./apiKeys";
import shares from "./shares";
import groups from "./groups";
import team from "./team";
import integrations from "./integrations";
import notificationSettings from "./notificationSettings";
import utils from "./utils";
import attachments from "./attachments";
import { NotFoundError } from '../errors';
import errorHandling from '../middlewares/errorHandling';
import validation from '../middlewares/validation';
import methodOverride from '../middlewares/methodOverride';
import cache from './middlewares/cache';
import apiWrapper from './middlewares/apiWrapper';
import editor from './middlewares/editor';
import { NotFoundError } from "../errors";
import errorHandling from "../middlewares/errorHandling";
import validation from "../middlewares/validation";
import methodOverride from "../middlewares/methodOverride";
import cache from "./middlewares/cache";
import apiWrapper from "./middlewares/apiWrapper";
import editor from "./middlewares/editor";
const api = new Koa();
const router = new Router();
@@ -41,25 +41,25 @@ api.use(apiWrapper());
api.use(editor());
// routes
router.use('/', auth.routes());
router.use('/', events.routes());
router.use('/', users.routes());
router.use('/', collections.routes());
router.use('/', documents.routes());
router.use('/', revisions.routes());
router.use('/', views.routes());
router.use('/', hooks.routes());
router.use('/', apiKeys.routes());
router.use('/', shares.routes());
router.use('/', team.routes());
router.use('/', integrations.routes());
router.use('/', notificationSettings.routes());
router.use('/', attachments.routes());
router.use('/', utils.routes());
router.use('/', groups.routes());
router.use("/", auth.routes());
router.use("/", events.routes());
router.use("/", users.routes());
router.use("/", collections.routes());
router.use("/", documents.routes());
router.use("/", revisions.routes());
router.use("/", views.routes());
router.use("/", hooks.routes());
router.use("/", apiKeys.routes());
router.use("/", shares.routes());
router.use("/", team.routes());
router.use("/", integrations.routes());
router.use("/", notificationSettings.routes());
router.use("/", attachments.routes());
router.use("/", utils.routes());
router.use("/", groups.routes());
router.post('*', ctx => {
ctx.throw(new NotFoundError('Endpoint not found'));
router.post("*", ctx => {
ctx.throw(new NotFoundError("Endpoint not found"));
});
// Router is embedded in a Koa application wrapper, because koa-router does not

View File

@@ -1,22 +1,22 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { flushdb } from '../test/support';
import TestServer from "fetch-test-server";
import app from "../app";
import { flushdb } from "../test/support";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('POST unknown endpoint', async () => {
it('should be not found', async () => {
const res = await server.post('/api/blah');
describe("POST unknown endpoint", async () => {
it("should be not found", async () => {
const res = await server.post("/api/blah");
expect(res.status).toEqual(404);
});
});
describe('GET unknown endpoint', async () => {
it('should be not found', async () => {
const res = await server.get('/api/blah');
describe("GET unknown endpoint", async () => {
it("should be not found", async () => {
const res = await server.get("/api/blah");
expect(res.status).toEqual(404);
});
});

View File

@@ -1,18 +1,18 @@
// @flow
import Router from 'koa-router';
import Integration from '../models/Integration';
import pagination from './middlewares/pagination';
import auth from '../middlewares/authentication';
import { Event } from '../models';
import { presentIntegration } from '../presenters';
import policy from '../policies';
import Router from "koa-router";
import Integration from "../models/Integration";
import pagination from "./middlewares/pagination";
import auth from "../middlewares/authentication";
import { Event } from "../models";
import { presentIntegration } from "../presenters";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('integrations.list', auth(), pagination(), async ctx => {
let { sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
router.post("integrations.list", auth(), pagination(), async ctx => {
let { sort = "updatedAt", direction } = ctx.body;
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const integrations = await Integration.findAll({
@@ -28,18 +28,18 @@ router.post('integrations.list', auth(), pagination(), async ctx => {
};
});
router.post('integrations.delete', auth(), async ctx => {
router.post("integrations.delete", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const integration = await Integration.findByPk(id);
authorize(user, 'delete', integration);
authorize(user, "delete", integration);
await integration.destroy();
await Event.create({
name: 'integrations.delete',
name: "integrations.delete",
modelId: integration.id,
teamId: integration.teamId,
actorId: user.id,

View File

@@ -1,6 +1,6 @@
// @flow
import stream from 'stream';
import { type Context } from 'koa';
import stream from "stream";
import { type Context } from "koa";
export default function apiWrapper() {
return async function apiWrapperMiddleware(
@@ -12,7 +12,7 @@ export default function apiWrapper() {
const ok = ctx.status < 400;
if (
typeof ctx.body !== 'string' &&
typeof ctx.body !== "string" &&
!(ctx.body instanceof stream.Readable)
) {
// $FlowFixMe

View File

@@ -1,8 +1,8 @@
// @flow
import debug from 'debug';
import { type Context } from 'koa';
import debug from "debug";
import { type Context } from "koa";
const log = debug('cache');
const log = debug("cache");
export default function cache() {
return async function cacheMiddleware(ctx: Context, next: () => Promise<*>) {

View File

@@ -1,12 +1,12 @@
// @flow
import semver from 'semver';
import { type Context } from 'koa';
import pkg from 'rich-markdown-editor/package.json';
import { EditorUpdateError } from '../../errors';
import semver from "semver";
import { type Context } from "koa";
import pkg from "rich-markdown-editor/package.json";
import { EditorUpdateError } from "../../errors";
export default function editor() {
return async function editorMiddleware(ctx: Context, next: () => Promise<*>) {
const clientVersion = ctx.headers['x-editor-version'];
const clientVersion = ctx.headers["x-editor-version"];
// If the editor version on the client is behind the current version being
// served in production by either a minor (new features), or major (breaking

View File

@@ -1,7 +1,7 @@
// @flow
import querystring from 'querystring';
import { InvalidRequestError } from '../../errors';
import { type Context } from 'koa';
import querystring from "querystring";
import { InvalidRequestError } from "../../errors";
import { type Context } from "koa";
export default function pagination(options?: Object) {
return async function paginationMiddleware(

View File

@@ -1,54 +1,54 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../../app';
import { flushdb, seed } from '../../test/support';
import TestServer from "fetch-test-server";
import app from "../../app";
import { flushdb, seed } from "../../test/support";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#pagination', async () => {
it('should allow offset and limit', async () => {
describe("#pagination", async () => {
it("should allow offset and limit", async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
const res = await server.post("/api/users.list", {
body: { token: user.getJwtToken(), limit: 1, offset: 1 },
});
expect(res.status).toEqual(200);
});
it('should not allow negative limit', async () => {
it("should not allow negative limit", async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
const res = await server.post("/api/users.list", {
body: { token: user.getJwtToken(), limit: -1 },
});
expect(res.status).toEqual(400);
});
it('should not allow non-integer limit', async () => {
it("should not allow non-integer limit", async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
body: { token: user.getJwtToken(), limit: 'blah' },
const res = await server.post("/api/users.list", {
body: { token: user.getJwtToken(), limit: "blah" },
});
expect(res.status).toEqual(400);
});
it('should not allow negative offset', async () => {
it("should not allow negative offset", async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
const res = await server.post("/api/users.list", {
body: { token: user.getJwtToken(), offset: -1 },
});
expect(res.status).toEqual(400);
});
it('should not allow non-integer offset', async () => {
it("should not allow non-integer offset", async () => {
const { user } = await seed();
const res = await server.post('/api/users.list', {
body: { token: user.getJwtToken(), offset: 'blah' },
const res = await server.post("/api/users.list", {
body: { token: user.getJwtToken(), offset: "blah" },
});
expect(res.status).toEqual(400);

View File

@@ -1,20 +1,20 @@
// @flow
import Router from 'koa-router';
import Router from "koa-router";
import auth from '../middlewares/authentication';
import { NotificationSetting } from '../models';
import { presentNotificationSetting } from '../presenters';
import policy from '../policies';
import auth from "../middlewares/authentication";
import { NotificationSetting } from "../models";
import { presentNotificationSetting } from "../presenters";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('notificationSettings.create', auth(), async ctx => {
router.post("notificationSettings.create", auth(), async ctx => {
const { event } = ctx.body;
ctx.assertPresent(event, 'event is required');
ctx.assertPresent(event, "event is required");
const user = ctx.state.user;
authorize(user, 'create', NotificationSetting);
authorize(user, "create", NotificationSetting);
const [setting] = await NotificationSetting.findOrCreate({
where: {
@@ -29,7 +29,7 @@ router.post('notificationSettings.create', auth(), async ctx => {
};
});
router.post('notificationSettings.list', auth(), async ctx => {
router.post("notificationSettings.list", auth(), async ctx => {
const user = ctx.state.user;
const settings = await NotificationSetting.findAll({
where: {
@@ -42,13 +42,13 @@ router.post('notificationSettings.list', auth(), async ctx => {
};
});
router.post('notificationSettings.delete', auth(), async ctx => {
router.post("notificationSettings.delete", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const setting = await NotificationSetting.findByPk(id);
authorize(user, 'delete', setting);
authorize(user, "delete", setting);
await setting.destroy();
@@ -57,10 +57,10 @@ router.post('notificationSettings.delete', auth(), async ctx => {
};
});
router.post('notificationSettings.unsubscribe', async ctx => {
router.post("notificationSettings.unsubscribe", async ctx => {
const { id, token } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertPresent(token, 'token is required');
ctx.assertUuid(id, "id is required");
ctx.assertPresent(token, "token is required");
const setting = await NotificationSetting.findByPk(id);
if (setting) {

View File

@@ -1,18 +1,18 @@
// @flow
import Router from 'koa-router';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import { presentRevision } from '../presenters';
import { Document, Revision } from '../models';
import { NotFoundError } from '../errors';
import policy from '../policies';
import Router from "koa-router";
import auth from "../middlewares/authentication";
import pagination from "./middlewares/pagination";
import { presentRevision } from "../presenters";
import { Document, Revision } from "../models";
import { NotFoundError } from "../errors";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('revisions.info', auth(), async ctx => {
router.post("revisions.info", auth(), async ctx => {
let { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(id, "id is required");
const user = ctx.state.user;
const revision = await Revision.findByPk(id);
@@ -23,7 +23,7 @@ router.post('revisions.info', auth(), async ctx => {
const document = await Document.findByPk(revision.documentId, {
userId: user.id,
});
authorize(user, 'read', document);
authorize(user, "read", document);
ctx.body = {
pagination: ctx.state.pagination,
@@ -31,14 +31,14 @@ router.post('revisions.info', auth(), async ctx => {
};
});
router.post('revisions.list', auth(), pagination(), async ctx => {
let { documentId, sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
ctx.assertPresent(documentId, 'documentId is required');
router.post("revisions.list", auth(), pagination(), async ctx => {
let { documentId, sort = "updatedAt", direction } = ctx.body;
if (direction !== "ASC") direction = "DESC";
ctx.assertPresent(documentId, "documentId is required");
const user = ctx.state.user;
const document = await Document.findByPk(documentId, { userId: user.id });
authorize(user, 'read', document);
authorize(user, "read", document);
const revisions = await Revision.findAll({
where: { documentId: document.id },

View File

@@ -1,24 +1,24 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { flushdb, seed } from '../test/support';
import { buildDocument, buildUser } from '../test/factories';
import Revision from '../models/Revision';
import TestServer from "fetch-test-server";
import app from "../app";
import { flushdb, seed } from "../test/support";
import { buildDocument, buildUser } from "../test/factories";
import Revision from "../models/Revision";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#revisions.info', async () => {
it('should return a document revision', async () => {
describe("#revisions.info", async () => {
it("should return a document revision", async () => {
const { user, document } = await seed();
const revision = await Revision.findOne({
where: {
documentId: document.id,
},
});
const res = await server.post('/api/revisions.info', {
const res = await server.post("/api/revisions.info", {
body: {
token: user.getJwtToken(),
id: revision.id,
@@ -31,7 +31,7 @@ describe('#revisions.info', async () => {
expect(body.data.title).toEqual(document.title);
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const document = await buildDocument();
const revision = await Revision.findOne({
where: {
@@ -39,7 +39,7 @@ describe('#revisions.info', async () => {
},
});
const user = await buildUser();
const res = await server.post('/api/revisions.info', {
const res = await server.post("/api/revisions.info", {
body: {
token: user.getJwtToken(),
id: revision.id,
@@ -49,10 +49,10 @@ describe('#revisions.info', async () => {
});
});
describe('#revisions.list', async () => {
describe("#revisions.list", async () => {
it("should return a document's revisions", async () => {
const { user, document } = await seed();
const res = await server.post('/api/revisions.list', {
const res = await server.post("/api/revisions.list", {
body: {
token: user.getJwtToken(),
documentId: document.id,
@@ -66,22 +66,22 @@ describe('#revisions.list', async () => {
expect(body.data[0].title).toEqual(document.title);
});
it('should not return revisions for document in collection not a member of', async () => {
it("should not return revisions for document in collection not a member of", async () => {
const { user, document, collection } = await seed();
collection.private = true;
await collection.save();
const res = await server.post('/api/revisions.list', {
const res = await server.post("/api/revisions.list", {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(403);
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const document = await buildDocument();
const user = await buildUser();
const res = await server.post('/api/revisions.list', {
const res = await server.post("/api/revisions.list", {
body: {
token: user.getJwtToken(),
documentId: document.id,

View File

@@ -1,32 +1,32 @@
// @flow
import Router from 'koa-router';
import Sequelize from 'sequelize';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import { presentShare } from '../presenters';
import { Document, User, Event, Share, Team } from '../models';
import policy from '../policies';
import Router from "koa-router";
import Sequelize from "sequelize";
import auth from "../middlewares/authentication";
import pagination from "./middlewares/pagination";
import { presentShare } from "../presenters";
import { Document, User, Event, Share, Team } from "../models";
import policy from "../policies";
const Op = Sequelize.Op;
const { authorize } = policy;
const router = new Router();
router.post('shares.info', auth(), async ctx => {
router.post("shares.info", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const share = await Share.findByPk(id);
authorize(user, 'read', share);
authorize(user, "read", share);
ctx.body = {
data: presentShare(share),
};
});
router.post('shares.list', auth(), pagination(), async ctx => {
let { sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
router.post("shares.list", auth(), pagination(), async ctx => {
let { sort = "updatedAt", direction } = ctx.body;
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const where = {
@@ -47,7 +47,7 @@ router.post('shares.list', auth(), pagination(), async ctx => {
{
model: Document,
required: true,
as: 'document',
as: "document",
where: {
collectionId: collectionIds,
},
@@ -55,7 +55,7 @@ router.post('shares.list', auth(), pagination(), async ctx => {
{
model: User,
required: true,
as: 'user',
as: "user",
},
],
offset: ctx.state.pagination.offset,
@@ -68,15 +68,15 @@ router.post('shares.list', auth(), pagination(), async ctx => {
};
});
router.post('shares.create', auth(), async ctx => {
router.post("shares.create", auth(), async ctx => {
const { documentId } = ctx.body;
ctx.assertPresent(documentId, 'documentId is required');
ctx.assertPresent(documentId, "documentId is required");
const user = ctx.state.user;
const document = await Document.findByPk(documentId, { userId: user.id });
const team = await Team.findByPk(user.teamId);
authorize(user, 'share', document);
authorize(user, 'share', team);
authorize(user, "share", document);
authorize(user, "share", team);
const [share] = await Share.findOrCreate({
where: {
@@ -88,7 +88,7 @@ router.post('shares.create', auth(), async ctx => {
});
await Event.create({
name: 'shares.create',
name: "shares.create",
documentId,
collectionId: document.collectionId,
modelId: share.id,
@@ -106,20 +106,20 @@ router.post('shares.create', auth(), async ctx => {
};
});
router.post('shares.revoke', auth(), async ctx => {
router.post("shares.revoke", auth(), async ctx => {
const { id } = ctx.body;
ctx.assertUuid(id, 'id is required');
ctx.assertUuid(id, "id is required");
const user = ctx.state.user;
const share = await Share.findByPk(id);
authorize(user, 'revoke', share);
authorize(user, "revoke", share);
await share.revoke(user.id);
const document = await Document.findByPk(share.documentId);
await Event.create({
name: 'shares.revoke',
name: "shares.revoke",
documentId: document.id,
collectionId: document.collectionId,
modelId: share.id,

View File

@@ -1,17 +1,17 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { CollectionUser } from '../models';
import { flushdb, seed } from '../test/support';
import { buildUser, buildShare } from '../test/factories';
import TestServer from "fetch-test-server";
import app from "../app";
import { CollectionUser } from "../models";
import { flushdb, seed } from "../test/support";
import { buildUser, buildShare } from "../test/factories";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#shares.list', async () => {
it('should only return shares created by user', async () => {
describe("#shares.list", async () => {
it("should only return shares created by user", async () => {
const { user, admin, document } = await seed();
await buildShare({
documentId: document.id,
@@ -23,7 +23,7 @@ describe('#shares.list', async () => {
teamId: user.teamId,
userId: user.id,
});
const res = await server.post('/api/shares.list', {
const res = await server.post("/api/shares.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
@@ -34,7 +34,7 @@ describe('#shares.list', async () => {
expect(body.data[0].documentTitle).toBe(document.title);
});
it('should not return revoked shares', async () => {
it("should not return revoked shares", async () => {
const { user, document } = await seed();
const share = await buildShare({
documentId: document.id,
@@ -43,7 +43,7 @@ describe('#shares.list', async () => {
});
await share.revoke(user.id);
const res = await server.post('/api/shares.list', {
const res = await server.post("/api/shares.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
@@ -52,14 +52,14 @@ describe('#shares.list', async () => {
expect(body.data.length).toEqual(0);
});
it('admins should return shares created by all users', async () => {
it("admins should return shares created by all users", async () => {
const { user, admin, document } = await seed();
const share = await buildShare({
documentId: document.id,
teamId: admin.teamId,
userId: user.id,
});
const res = await server.post('/api/shares.list', {
const res = await server.post("/api/shares.list", {
body: { token: admin.getJwtToken() },
});
const body = await res.json();
@@ -70,7 +70,7 @@ describe('#shares.list', async () => {
expect(body.data[0].documentTitle).toBe(document.title);
});
it('admins should not return shares in collection not a member of', async () => {
it("admins should not return shares in collection not a member of", async () => {
const { admin, document, collection } = await seed();
await buildShare({
documentId: document.id,
@@ -81,7 +81,7 @@ describe('#shares.list', async () => {
collection.private = true;
await collection.save();
const res = await server.post('/api/shares.list', {
const res = await server.post("/api/shares.list", {
body: { token: admin.getJwtToken() },
});
const body = await res.json();
@@ -90,8 +90,8 @@ describe('#shares.list', async () => {
expect(body.data.length).toEqual(0);
});
it('should require authentication', async () => {
const res = await server.post('/api/shares.list');
it("should require authentication", async () => {
const res = await server.post("/api/shares.list");
const body = await res.json();
expect(res.status).toEqual(401);
@@ -99,10 +99,10 @@ describe('#shares.list', async () => {
});
});
describe('#shares.create', async () => {
it('should allow creating a share record for document', async () => {
describe("#shares.create", async () => {
it("should allow creating a share record for document", async () => {
const { user, document } = await seed();
const res = await server.post('/api/shares.create', {
const res = await server.post("/api/shares.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
const body = await res.json();
@@ -111,7 +111,7 @@ describe('#shares.create', async () => {
expect(body.data.documentTitle).toBe(document.title);
});
it('should allow creating a share record for document in read-only collection', async () => {
it("should allow creating a share record for document in read-only collection", async () => {
const { user, document, collection } = await seed();
collection.private = true;
await collection.save();
@@ -120,10 +120,10 @@ describe('#shares.create', async () => {
createdById: user.id,
collectionId: collection.id,
userId: user.id,
permission: 'read',
permission: "read",
});
const res = await server.post('/api/shares.create', {
const res = await server.post("/api/shares.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
const body = await res.json();
@@ -132,7 +132,7 @@ describe('#shares.create', async () => {
expect(body.data.documentTitle).toBe(document.title);
});
it('should allow creating a share record if link previously revoked', async () => {
it("should allow creating a share record if link previously revoked", async () => {
const { user, document } = await seed();
const share = await buildShare({
documentId: document.id,
@@ -140,7 +140,7 @@ describe('#shares.create', async () => {
userId: user.id,
});
await share.revoke();
const res = await server.post('/api/shares.create', {
const res = await server.post("/api/shares.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
const body = await res.json();
@@ -150,14 +150,14 @@ describe('#shares.create', async () => {
expect(body.data.documentTitle).toBe(document.title);
});
it('should return existing share link for document and user', async () => {
it("should return existing share link for document and user", async () => {
const { user, document } = await seed();
const share = await buildShare({
documentId: document.id,
teamId: user.teamId,
userId: user.id,
});
const res = await server.post('/api/shares.create', {
const res = await server.post("/api/shares.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
const body = await res.json();
@@ -166,18 +166,18 @@ describe('#shares.create', async () => {
expect(body.data.id).toBe(share.id);
});
it('should not allow creating a share record if disabled', async () => {
it("should not allow creating a share record if disabled", async () => {
const { user, document, team } = await seed();
await team.update({ sharing: false });
const res = await server.post('/api/shares.create', {
const res = await server.post("/api/shares.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(403);
});
it('should require authentication', async () => {
it("should require authentication", async () => {
const { document } = await seed();
const res = await server.post('/api/shares.create', {
const res = await server.post("/api/shares.create", {
body: { documentId: document.id },
});
const body = await res.json();
@@ -186,18 +186,18 @@ describe('#shares.create', async () => {
expect(body).toMatchSnapshot();
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const { document } = await seed();
const user = await buildUser();
const res = await server.post('/api/shares.create', {
const res = await server.post("/api/shares.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(403);
});
});
describe('#shares.revoke', async () => {
it('should allow author to revoke a share', async () => {
describe("#shares.revoke", async () => {
it("should allow author to revoke a share", async () => {
const { user, document } = await seed();
const share = await buildShare({
documentId: document.id,
@@ -205,13 +205,13 @@ describe('#shares.revoke', async () => {
userId: user.id,
});
const res = await server.post('/api/shares.revoke', {
const res = await server.post("/api/shares.revoke", {
body: { token: user.getJwtToken(), id: share.id },
});
expect(res.status).toEqual(200);
});
it('should allow admin to revoke a share', async () => {
it("should allow admin to revoke a share", async () => {
const { user, admin, document } = await seed();
const share = await buildShare({
documentId: document.id,
@@ -219,20 +219,20 @@ describe('#shares.revoke', async () => {
userId: user.id,
});
const res = await server.post('/api/shares.revoke', {
const res = await server.post("/api/shares.revoke", {
body: { token: admin.getJwtToken(), id: share.id },
});
expect(res.status).toEqual(200);
});
it('should require authentication', async () => {
it("should require authentication", async () => {
const { user, document } = await seed();
const share = await buildShare({
documentId: document.id,
teamId: user.teamId,
userId: user.id,
});
const res = await server.post('/api/shares.revoke', {
const res = await server.post("/api/shares.revoke", {
body: { id: share.id },
});
const body = await res.json();
@@ -241,10 +241,10 @@ describe('#shares.revoke', async () => {
expect(body).toMatchSnapshot();
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const { document } = await seed();
const user = await buildUser();
const res = await server.post('/api/shares.create', {
const res = await server.post("/api/shares.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(403);

View File

@@ -1,15 +1,15 @@
// @flow
import Router from 'koa-router';
import { Team } from '../models';
import Router from "koa-router";
import { Team } from "../models";
import auth from '../middlewares/authentication';
import { presentTeam, presentPolicies } from '../presenters';
import policy from '../policies';
import auth from "../middlewares/authentication";
import { presentTeam, presentPolicies } from "../presenters";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('team.update', auth(), async ctx => {
router.post("team.update", auth(), async ctx => {
const {
name,
avatarUrl,
@@ -20,10 +20,10 @@ router.post('team.update', auth(), async ctx => {
} = ctx.body;
const user = ctx.state.user;
const team = await Team.findByPk(user.teamId);
authorize(user, 'update', team);
authorize(user, "update", team);
if (subdomain !== undefined && process.env.SUBDOMAINS_ENABLED === 'true') {
team.subdomain = subdomain === '' ? null : subdomain;
if (subdomain !== undefined && process.env.SUBDOMAINS_ENABLED === "true") {
team.subdomain = subdomain === "" ? null : subdomain;
}
if (name) team.name = name;

View File

@@ -1,37 +1,37 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import TestServer from "fetch-test-server";
import app from "../app";
import { flushdb, seed } from '../test/support';
import { flushdb, seed } from "../test/support";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#team.update', async () => {
it('should update team details', async () => {
describe("#team.update", async () => {
it("should update team details", async () => {
const { admin } = await seed();
const res = await server.post('/api/team.update', {
body: { token: admin.getJwtToken(), name: 'New name' },
const res = await server.post("/api/team.update", {
body: { token: admin.getJwtToken(), name: "New name" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.name).toEqual('New name');
expect(body.data.name).toEqual("New name");
});
it('should require admin', async () => {
it("should require admin", async () => {
const { user } = await seed();
const res = await server.post('/api/team.update', {
body: { token: user.getJwtToken(), name: 'New name' },
const res = await server.post("/api/team.update", {
body: { token: user.getJwtToken(), name: "New name" },
});
expect(res.status).toEqual(403);
});
it('should require authentication', async () => {
it("should require authentication", async () => {
await seed();
const res = await server.post('/api/team.update');
const res = await server.post("/api/team.update");
expect(res.status).toEqual(401);
});
});

View File

@@ -1,20 +1,20 @@
// @flow
import Router from 'koa-router';
import { Op } from '../sequelize';
import { Event, User, Team } from '../models';
import auth from '../middlewares/authentication';
import pagination from './middlewares/pagination';
import userInviter from '../commands/userInviter';
import { presentUser } from '../presenters';
import policy from '../policies';
import Router from "koa-router";
import { Op } from "../sequelize";
import { Event, User, Team } from "../models";
import auth from "../middlewares/authentication";
import pagination from "./middlewares/pagination";
import userInviter from "../commands/userInviter";
import { presentUser } from "../presenters";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('users.list', auth(), pagination(), async ctx => {
const { sort = 'createdAt', query, includeSuspended = false } = ctx.body;
router.post("users.list", auth(), pagination(), async ctx => {
const { sort = "createdAt", query, includeSuspended = false } = ctx.body;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
let where = {
@@ -54,13 +54,13 @@ router.post('users.list', auth(), pagination(), async ctx => {
};
});
router.post('users.info', auth(), async ctx => {
router.post("users.info", auth(), async ctx => {
ctx.body = {
data: presentUser(ctx.state.user),
};
});
router.post('users.update', auth(), async ctx => {
router.post("users.update", auth(), async ctx => {
const { user } = ctx.state;
const { name, avatarUrl } = ctx.body;
@@ -76,19 +76,19 @@ router.post('users.update', auth(), async ctx => {
// Admin specific
router.post('users.promote', auth(), async ctx => {
router.post("users.promote", auth(), async ctx => {
const userId = ctx.body.id;
const teamId = ctx.state.user.teamId;
ctx.assertPresent(userId, 'id is required');
ctx.assertPresent(userId, "id is required");
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'promote', user);
authorize(ctx.state.user, "promote", user);
const team = await Team.findByPk(teamId);
await team.addAdmin(user);
await Event.create({
name: 'users.promote',
name: "users.promote",
actorId: ctx.state.user.id,
userId,
teamId,
@@ -101,19 +101,19 @@ router.post('users.promote', auth(), async ctx => {
};
});
router.post('users.demote', auth(), async ctx => {
router.post("users.demote", auth(), async ctx => {
const userId = ctx.body.id;
const teamId = ctx.state.user.teamId;
ctx.assertPresent(userId, 'id is required');
ctx.assertPresent(userId, "id is required");
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'demote', user);
authorize(ctx.state.user, "demote", user);
const team = await Team.findByPk(teamId);
await team.removeAdmin(user);
await Event.create({
name: 'users.demote',
name: "users.demote",
actorId: ctx.state.user.id,
userId,
teamId,
@@ -126,20 +126,20 @@ router.post('users.demote', auth(), async ctx => {
};
});
router.post('users.suspend', auth(), async ctx => {
router.post("users.suspend", auth(), async ctx => {
const admin = ctx.state.user;
const userId = ctx.body.id;
const teamId = ctx.state.user.teamId;
ctx.assertPresent(userId, 'id is required');
ctx.assertPresent(userId, "id is required");
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'suspend', user);
authorize(ctx.state.user, "suspend", user);
const team = await Team.findByPk(teamId);
await team.suspendUser(user, admin);
await Event.create({
name: 'users.suspend',
name: "users.suspend",
actorId: ctx.state.user.id,
userId,
teamId,
@@ -152,20 +152,20 @@ router.post('users.suspend', auth(), async ctx => {
};
});
router.post('users.activate', auth(), async ctx => {
router.post("users.activate", auth(), async ctx => {
const admin = ctx.state.user;
const userId = ctx.body.id;
const teamId = ctx.state.user.teamId;
ctx.assertPresent(userId, 'id is required');
ctx.assertPresent(userId, "id is required");
const user = await User.findByPk(userId);
authorize(ctx.state.user, 'activate', user);
authorize(ctx.state.user, "activate", user);
const team = await Team.findByPk(teamId);
await team.activateUser(user, admin);
await Event.create({
name: 'users.activate',
name: "users.activate",
actorId: ctx.state.user.id,
userId,
teamId,
@@ -178,12 +178,12 @@ router.post('users.activate', auth(), async ctx => {
};
});
router.post('users.invite', auth(), async ctx => {
router.post("users.invite", auth(), async ctx => {
const { invites } = ctx.body;
ctx.assertPresent(invites, 'invites is required');
ctx.assertPresent(invites, "invites is required");
const user = ctx.state.user;
authorize(user, 'invite', User);
authorize(user, "invite", User);
const response = await userInviter({ user, invites, ip: ctx.request.ip });
@@ -195,17 +195,17 @@ router.post('users.invite', auth(), async ctx => {
};
});
router.post('users.delete', auth(), async ctx => {
router.post("users.delete", auth(), async ctx => {
const { confirmation, id } = ctx.body;
ctx.assertPresent(confirmation, 'confirmation is required');
ctx.assertPresent(confirmation, "confirmation is required");
let user = ctx.state.user;
if (id) user = await User.findByPk(id);
authorize(ctx.state.user, 'delete', user);
authorize(ctx.state.user, "delete", user);
await user.destroy();
await Event.create({
name: 'users.delete',
name: "users.delete",
actorId: user.id,
userId: user.id,
teamId: user.teamId,

View File

@@ -1,29 +1,29 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import TestServer from "fetch-test-server";
import app from "../app";
import { flushdb, seed } from '../test/support';
import { buildUser } from '../test/factories';
import { flushdb, seed } from "../test/support";
import { buildUser } from "../test/factories";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#users.list', async () => {
it('should allow filtering by user name', async () => {
const user = await buildUser({ name: 'Tester' });
describe("#users.list", async () => {
it("should allow filtering by user name", async () => {
const user = await buildUser({ name: "Tester" });
// suspended user should not be returned
await buildUser({
name: 'Tester',
name: "Tester",
teamId: user.teamId,
suspendedAt: new Date(),
});
const res = await server.post('/api/users.list', {
const res = await server.post("/api/users.list", {
body: {
query: 'test',
query: "test",
token: user.getJwtToken(),
},
});
@@ -34,17 +34,17 @@ describe('#users.list', async () => {
expect(body.data[0].id).toEqual(user.id);
});
it('should allow including suspended', async () => {
const user = await buildUser({ name: 'Tester' });
it("should allow including suspended", async () => {
const user = await buildUser({ name: "Tester" });
await buildUser({
name: 'Tester',
name: "Tester",
teamId: user.teamId,
suspendedAt: new Date(),
});
const res = await server.post('/api/users.list', {
const res = await server.post("/api/users.list", {
body: {
query: 'test',
query: "test",
includeSuspended: true,
token: user.getJwtToken(),
},
@@ -55,10 +55,10 @@ describe('#users.list', async () => {
expect(body.data.length).toEqual(2);
});
it('should return teams paginated user list', async () => {
it("should return teams paginated user list", async () => {
const { admin, user } = await seed();
const res = await server.post('/api/users.list', {
const res = await server.post("/api/users.list", {
body: { token: admin.getJwtToken() },
});
const body = await res.json();
@@ -69,9 +69,9 @@ describe('#users.list', async () => {
expect(body.data[1].id).toEqual(admin.id);
});
it('should require admin for detailed info', async () => {
it("should require admin for detailed info", async () => {
const { user, admin } = await seed();
const res = await server.post('/api/users.list', {
const res = await server.post("/api/users.list", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
@@ -85,10 +85,10 @@ describe('#users.list', async () => {
});
});
describe('#users.info', async () => {
it('should return known user', async () => {
describe("#users.info", async () => {
it("should return known user", async () => {
const user = await buildUser();
const res = await server.post('/api/users.info', {
const res = await server.post("/api/users.info", {
body: { token: user.getJwtToken() },
});
const body = await res.json();
@@ -98,19 +98,19 @@ describe('#users.info', async () => {
expect(body.data.name).toEqual(user.name);
});
it('should require authentication', async () => {
const res = await server.post('/api/users.info');
it("should require authentication", async () => {
const res = await server.post("/api/users.info");
expect(res.status).toEqual(401);
});
});
describe('#users.invite', async () => {
it('should return sent invites', async () => {
describe("#users.invite", async () => {
it("should return sent invites", async () => {
const user = await buildUser();
const res = await server.post('/api/users.invite', {
const res = await server.post("/api/users.invite", {
body: {
token: user.getJwtToken(),
invites: [{ email: 'test@example.com', name: 'Test', guest: false }],
invites: [{ email: "test@example.com", name: "Test", guest: false }],
},
});
const body = await res.json();
@@ -118,70 +118,70 @@ describe('#users.invite', async () => {
expect(body.data.sent.length).toEqual(1);
});
it('should require authentication', async () => {
const res = await server.post('/api/users.invite');
it("should require authentication", async () => {
const res = await server.post("/api/users.invite");
expect(res.status).toEqual(401);
});
});
describe('#users.delete', async () => {
it('should not allow deleting without confirmation', async () => {
describe("#users.delete", async () => {
it("should not allow deleting without confirmation", async () => {
const user = await buildUser();
const res = await server.post('/api/users.delete', {
const res = await server.post("/api/users.delete", {
body: { token: user.getJwtToken() },
});
expect(res.status).toEqual(400);
});
it('should allow deleting last admin if only user', async () => {
it("should allow deleting last admin if only user", async () => {
const user = await buildUser({ isAdmin: true });
const res = await server.post('/api/users.delete', {
const res = await server.post("/api/users.delete", {
body: { token: user.getJwtToken(), confirmation: true },
});
expect(res.status).toEqual(200);
});
it('should not allow deleting last admin if many users', async () => {
it("should not allow deleting last admin if many users", async () => {
const user = await buildUser({ isAdmin: true });
await buildUser({ teamId: user.teamId, isAdmin: false });
const res = await server.post('/api/users.delete', {
const res = await server.post("/api/users.delete", {
body: { token: user.getJwtToken(), confirmation: true },
});
expect(res.status).toEqual(400);
});
it('should allow deleting user account with confirmation', async () => {
it("should allow deleting user account with confirmation", async () => {
const user = await buildUser();
const res = await server.post('/api/users.delete', {
const res = await server.post("/api/users.delete", {
body: { token: user.getJwtToken(), confirmation: true },
});
expect(res.status).toEqual(200);
});
it('should allow deleting pending user account with admin', async () => {
it("should allow deleting pending user account with admin", async () => {
const user = await buildUser({ isAdmin: true });
const pending = await buildUser({
teamId: user.teamId,
lastActiveAt: null,
});
const res = await server.post('/api/users.delete', {
const res = await server.post("/api/users.delete", {
body: { token: user.getJwtToken(), id: pending.id, confirmation: true },
});
expect(res.status).toEqual(200);
});
it('should not allow deleting another user account', async () => {
it("should not allow deleting another user account", async () => {
const user = await buildUser({ isAdmin: true });
const user2 = await buildUser({ teamId: user.teamId });
const res = await server.post('/api/users.delete', {
const res = await server.post("/api/users.delete", {
body: { token: user.getJwtToken(), id: user2.id, confirmation: true },
});
expect(res.status).toEqual(403);
});
it('should require authentication', async () => {
const res = await server.post('/api/users.delete');
it("should require authentication", async () => {
const res = await server.post("/api/users.delete");
const body = await res.json();
expect(res.status).toEqual(401);
@@ -189,20 +189,20 @@ describe('#users.delete', async () => {
});
});
describe('#users.update', async () => {
it('should update user profile information', async () => {
describe("#users.update", async () => {
it("should update user profile information", async () => {
const { user } = await seed();
const res = await server.post('/api/users.update', {
body: { token: user.getJwtToken(), name: 'New name' },
const res = await server.post("/api/users.update", {
body: { token: user.getJwtToken(), name: "New name" },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.name).toEqual('New name');
expect(body.data.name).toEqual("New name");
});
it('should require authentication', async () => {
const res = await server.post('/api/users.update');
it("should require authentication", async () => {
const res = await server.post("/api/users.update");
const body = await res.json();
expect(res.status).toEqual(401);
@@ -210,11 +210,11 @@ describe('#users.update', async () => {
});
});
describe('#users.promote', async () => {
it('should promote a new admin', async () => {
describe("#users.promote", async () => {
it("should promote a new admin", async () => {
const { admin, user } = await seed();
const res = await server.post('/api/users.promote', {
const res = await server.post("/api/users.promote", {
body: { token: admin.getJwtToken(), id: user.id },
});
const body = await res.json();
@@ -223,9 +223,9 @@ describe('#users.promote', async () => {
expect(body).toMatchSnapshot();
});
it('should require admin', async () => {
it("should require admin", async () => {
const user = await buildUser();
const res = await server.post('/api/users.promote', {
const res = await server.post("/api/users.promote", {
body: { token: user.getJwtToken(), id: user.id },
});
const body = await res.json();
@@ -235,12 +235,12 @@ describe('#users.promote', async () => {
});
});
describe('#users.demote', async () => {
it('should demote an admin', async () => {
describe("#users.demote", async () => {
it("should demote an admin", async () => {
const { admin, user } = await seed();
await user.update({ isAdmin: true }); // Make another admin
const res = await server.post('/api/users.demote', {
const res = await server.post("/api/users.demote", {
body: {
token: admin.getJwtToken(),
id: user.id,
@@ -252,10 +252,10 @@ describe('#users.demote', async () => {
expect(body).toMatchSnapshot();
});
it('should not demote admins if only one available', async () => {
it("should not demote admins if only one available", async () => {
const admin = await buildUser({ isAdmin: true });
const res = await server.post('/api/users.demote', {
const res = await server.post("/api/users.demote", {
body: {
token: admin.getJwtToken(),
id: admin.id,
@@ -267,9 +267,9 @@ describe('#users.demote', async () => {
expect(body).toMatchSnapshot();
});
it('should require admin', async () => {
it("should require admin", async () => {
const user = await buildUser();
const res = await server.post('/api/users.promote', {
const res = await server.post("/api/users.promote", {
body: { token: user.getJwtToken(), id: user.id },
});
const body = await res.json();
@@ -279,11 +279,11 @@ describe('#users.demote', async () => {
});
});
describe('#users.suspend', async () => {
it('should suspend an user', async () => {
describe("#users.suspend", async () => {
it("should suspend an user", async () => {
const { admin, user } = await seed();
const res = await server.post('/api/users.suspend', {
const res = await server.post("/api/users.suspend", {
body: {
token: admin.getJwtToken(),
id: user.id,
@@ -295,9 +295,9 @@ describe('#users.suspend', async () => {
expect(body).toMatchSnapshot();
});
it('should not allow suspending the user themselves', async () => {
it("should not allow suspending the user themselves", async () => {
const admin = await buildUser({ isAdmin: true });
const res = await server.post('/api/users.suspend', {
const res = await server.post("/api/users.suspend", {
body: {
token: admin.getJwtToken(),
id: admin.id,
@@ -309,9 +309,9 @@ describe('#users.suspend', async () => {
expect(body).toMatchSnapshot();
});
it('should require admin', async () => {
it("should require admin", async () => {
const user = await buildUser();
const res = await server.post('/api/users.suspend', {
const res = await server.post("/api/users.suspend", {
body: { token: user.getJwtToken(), id: user.id },
});
const body = await res.json();
@@ -321,8 +321,8 @@ describe('#users.suspend', async () => {
});
});
describe('#users.activate', async () => {
it('should activate a suspended user', async () => {
describe("#users.activate", async () => {
it("should activate a suspended user", async () => {
const { admin, user } = await seed();
await user.update({
suspendedById: admin.id,
@@ -330,7 +330,7 @@ describe('#users.activate', async () => {
});
expect(user.isSuspended).toBe(true);
const res = await server.post('/api/users.activate', {
const res = await server.post("/api/users.activate", {
body: {
token: admin.getJwtToken(),
id: user.id,
@@ -342,9 +342,9 @@ describe('#users.activate', async () => {
expect(body).toMatchSnapshot();
});
it('should require admin', async () => {
it("should require admin", async () => {
const user = await buildUser();
const res = await server.post('/api/users.activate', {
const res = await server.post("/api/users.activate", {
body: { token: user.getJwtToken(), id: user.id },
});
const body = await res.json();

View File

@@ -1,22 +1,22 @@
// @flow
import debug from 'debug';
import Router from 'koa-router';
import subDays from 'date-fns/sub_days';
import { AuthenticationError } from '../errors';
import { Document, Attachment } from '../models';
import { Op } from '../sequelize';
import debug from "debug";
import Router from "koa-router";
import subDays from "date-fns/sub_days";
import { AuthenticationError } from "../errors";
import { Document, Attachment } from "../models";
import { Op } from "../sequelize";
const router = new Router();
const log = debug('utils');
const log = debug("utils");
router.post('utils.gc', async ctx => {
router.post("utils.gc", async ctx => {
const { token } = ctx.body;
if (process.env.UTILS_SECRET !== token) {
throw new AuthenticationError('Invalid secret token');
throw new AuthenticationError("Invalid secret token");
}
log('Permanently deleting documents older than 30 days…');
log("Permanently deleting documents older than 30 days…");
const where = {
deletedAt: {
@@ -24,8 +24,8 @@ router.post('utils.gc', async ctx => {
},
};
const documents = await Document.scope('withUnpublished').findAll({
attributes: ['id'],
const documents = await Document.scope("withUnpublished").findAll({
attributes: ["id"],
where,
});
const documentIds = documents.map(d => d.id);
@@ -36,7 +36,7 @@ router.post('utils.gc', async ctx => {
},
});
await Document.scope('withUnpublished').destroy({
await Document.scope("withUnpublished").destroy({
where,
force: true,
});

View File

@@ -1,19 +1,19 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import subDays from 'date-fns/sub_days';
import app from '../app';
import { Document } from '../models';
import { sequelize } from '../sequelize';
import { flushdb } from '../test/support';
import { buildDocument } from '../test/factories';
import TestServer from "fetch-test-server";
import subDays from "date-fns/sub_days";
import app from "../app";
import { Document } from "../models";
import { sequelize } from "../sequelize";
import { flushdb } from "../test/support";
import { buildDocument } from "../test/factories";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#utils.gc', async () => {
it('should destroy documents deleted more than 30 days ago', async () => {
describe("#utils.gc", async () => {
it("should destroy documents deleted more than 30 days ago", async () => {
const document = await buildDocument({
publishedAt: new Date(),
});
@@ -25,7 +25,7 @@ describe('#utils.gc', async () => {
).toISOString()}' WHERE id = '${document.id}'`
);
const res = await server.post('/api/utils.gc', {
const res = await server.post("/api/utils.gc", {
body: {
token: process.env.UTILS_SECRET,
},
@@ -40,7 +40,7 @@ describe('#utils.gc', async () => {
expect(reloaded).toBe(null);
});
it('should destroy draft documents deleted more than 30 days ago', async () => {
it("should destroy draft documents deleted more than 30 days ago", async () => {
const document = await buildDocument({
publishedAt: undefined,
});
@@ -52,7 +52,7 @@ describe('#utils.gc', async () => {
).toISOString()}' WHERE id = '${document.id}'`
);
const res = await server.post('/api/utils.gc', {
const res = await server.post("/api/utils.gc", {
body: {
token: process.env.UTILS_SECRET,
},
@@ -67,8 +67,8 @@ describe('#utils.gc', async () => {
expect(reloaded).toBe(null);
});
it('should require authentication', async () => {
const res = await server.post('/api/utils.gc');
it("should require authentication", async () => {
const res = await server.post("/api/utils.gc");
expect(res.status).toEqual(401);
});
});

View File

@@ -1,20 +1,20 @@
// @flow
import Router from 'koa-router';
import auth from '../middlewares/authentication';
import { presentView } from '../presenters';
import { View, Document, Event } from '../models';
import policy from '../policies';
import Router from "koa-router";
import auth from "../middlewares/authentication";
import { presentView } from "../presenters";
import { View, Document, Event } from "../models";
import policy from "../policies";
const { authorize } = policy;
const router = new Router();
router.post('views.list', auth(), async ctx => {
router.post("views.list", auth(), async ctx => {
const { documentId } = ctx.body;
ctx.assertUuid(documentId, 'documentId is required');
ctx.assertUuid(documentId, "documentId is required");
const user = ctx.state.user;
const document = await Document.findByPk(documentId, { userId: user.id });
authorize(user, 'read', document);
authorize(user, "read", document);
const views = await View.findByDocument(documentId);
@@ -23,18 +23,18 @@ router.post('views.list', auth(), async ctx => {
};
});
router.post('views.create', auth(), async ctx => {
router.post("views.create", auth(), async ctx => {
const { documentId } = ctx.body;
ctx.assertUuid(documentId, 'documentId is required');
ctx.assertUuid(documentId, "documentId is required");
const user = ctx.state.user;
const document = await Document.findByPk(documentId, { userId: user.id });
authorize(user, 'read', document);
authorize(user, "read", document);
const view = await View.increment({ documentId, userId: user.id });
await Event.create({
name: 'views.create',
name: "views.create",
actorId: user.id,
documentId: document.id,
collectionId: document.collectionId,

View File

@@ -1,21 +1,21 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '../app';
import { View, CollectionUser } from '../models';
import { flushdb, seed } from '../test/support';
import { buildUser } from '../test/factories';
import TestServer from "fetch-test-server";
import app from "../app";
import { View, CollectionUser } from "../models";
import { flushdb, seed } from "../test/support";
import { buildUser } from "../test/factories";
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#views.list', async () => {
it('should return views for a document', async () => {
describe("#views.list", async () => {
it("should return views for a document", async () => {
const { user, document } = await seed();
await View.increment({ documentId: document.id, userId: user.id });
const res = await server.post('/api/views.list', {
const res = await server.post("/api/views.list", {
body: { token: user.getJwtToken(), documentId: document.id },
});
const body = await res.json();
@@ -25,7 +25,7 @@ describe('#views.list', async () => {
expect(body.data[0].user.name).toBe(user.name);
});
it('should return views for a document in read-only collection', async () => {
it("should return views for a document in read-only collection", async () => {
const { user, document, collection } = await seed();
collection.private = true;
await collection.save();
@@ -34,12 +34,12 @@ describe('#views.list', async () => {
createdById: user.id,
collectionId: collection.id,
userId: user.id,
permission: 'read',
permission: "read",
});
await View.increment({ documentId: document.id, userId: user.id });
const res = await server.post('/api/views.list', {
const res = await server.post("/api/views.list", {
body: { token: user.getJwtToken(), documentId: document.id },
});
const body = await res.json();
@@ -49,9 +49,9 @@ describe('#views.list', async () => {
expect(body.data[0].user.name).toBe(user.name);
});
it('should require authentication', async () => {
it("should require authentication", async () => {
const { document } = await seed();
const res = await server.post('/api/views.list', {
const res = await server.post("/api/views.list", {
body: { documentId: document.id },
});
const body = await res.json();
@@ -60,20 +60,20 @@ describe('#views.list', async () => {
expect(body).toMatchSnapshot();
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const { document } = await seed();
const user = await buildUser();
const res = await server.post('/api/views.list', {
const res = await server.post("/api/views.list", {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(403);
});
});
describe('#views.create', async () => {
it('should allow creating a view record for document', async () => {
describe("#views.create", async () => {
it("should allow creating a view record for document", async () => {
const { user, document } = await seed();
const res = await server.post('/api/views.create', {
const res = await server.post("/api/views.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
const body = await res.json();
@@ -82,7 +82,7 @@ describe('#views.create', async () => {
expect(body.data.count).toBe(1);
});
it('should allow creating a view record for document in read-only collection', async () => {
it("should allow creating a view record for document in read-only collection", async () => {
const { user, document, collection } = await seed();
collection.private = true;
await collection.save();
@@ -91,10 +91,10 @@ describe('#views.create', async () => {
createdById: user.id,
collectionId: collection.id,
userId: user.id,
permission: 'read',
permission: "read",
});
const res = await server.post('/api/views.create', {
const res = await server.post("/api/views.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
const body = await res.json();
@@ -103,9 +103,9 @@ describe('#views.create', async () => {
expect(body.data.count).toBe(1);
});
it('should require authentication', async () => {
it("should require authentication", async () => {
const { document } = await seed();
const res = await server.post('/api/views.create', {
const res = await server.post("/api/views.create", {
body: { documentId: document.id },
});
const body = await res.json();
@@ -114,10 +114,10 @@ describe('#views.create', async () => {
expect(body).toMatchSnapshot();
});
it('should require authorization', async () => {
it("should require authorization", async () => {
const { document } = await seed();
const user = await buildUser();
const res = await server.post('/api/views.create', {
const res = await server.post("/api/views.create", {
body: { token: user.getJwtToken(), documentId: document.id },
});
expect(res.status).toEqual(403);