Collection Permissions (#829)
see https://github.com/outline/outline/issues/668
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`#collections.add_user should require user in team 1`] = `
|
||||
Object {
|
||||
"error": "authorization_error",
|
||||
"message": "Authorization error",
|
||||
"ok": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#collections.create should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
@@ -53,3 +61,20 @@ Object {
|
||||
"status": 401,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#collections.remove_user should require user in team 1`] = `
|
||||
Object {
|
||||
"error": "authorization_error",
|
||||
"message": "Authorization error",
|
||||
"ok": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#collections.users should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
"message": "Authentication required",
|
||||
"ok": false,
|
||||
"status": 401,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -48,7 +48,7 @@ router.post('apiKeys.list', auth(), pagination(), async ctx => {
|
||||
|
||||
router.post('apiKeys.delete', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const key = await ApiKey.findById(id);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
import Router from 'koa-router';
|
||||
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentCollection } from '../presenters';
|
||||
import { Collection, Team } from '../models';
|
||||
import { ValidationError } from '../errors';
|
||||
import { presentCollection, presentUser } from '../presenters';
|
||||
import { Collection, CollectionUser, Team, User } from '../models';
|
||||
import { ValidationError, InvalidRequestError } from '../errors';
|
||||
import { exportCollection, exportCollections } from '../logistics';
|
||||
import policy from '../policies';
|
||||
|
||||
@@ -14,6 +13,8 @@ const router = new Router();
|
||||
|
||||
router.post('collections.create', auth(), async ctx => {
|
||||
const { name, color, description, type } = ctx.body;
|
||||
const isPrivate = ctx.body.private;
|
||||
|
||||
ctx.assertPresent(name, 'name is required');
|
||||
if (color)
|
||||
ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)');
|
||||
@@ -28,6 +29,7 @@ router.post('collections.create', auth(), async ctx => {
|
||||
type: type || 'atlas',
|
||||
teamId: user.teamId,
|
||||
creatorId: user.id,
|
||||
private: isPrivate,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
@@ -37,9 +39,9 @@ router.post('collections.create', auth(), async ctx => {
|
||||
|
||||
router.post('collections.info', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const collection = await Collection.scope('withRecentDocuments').findById(id);
|
||||
const collection = await Collection.findById(id);
|
||||
authorize(ctx.state.user, 'read', collection);
|
||||
|
||||
ctx.body = {
|
||||
@@ -47,9 +49,76 @@ router.post('collections.info', auth(), async ctx => {
|
||||
};
|
||||
});
|
||||
|
||||
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.findById(id);
|
||||
authorize(ctx.state.user, 'update', collection);
|
||||
|
||||
if (!collection.private) {
|
||||
throw new InvalidRequestError('Collection must be private to add users');
|
||||
}
|
||||
|
||||
const user = await User.findById(userId);
|
||||
authorize(ctx.state.user, 'read', user);
|
||||
|
||||
await CollectionUser.create({
|
||||
collectionId: id,
|
||||
userId,
|
||||
permission,
|
||||
createdById: ctx.state.user.id,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
const collection = await Collection.findById(id);
|
||||
authorize(ctx.state.user, 'update', collection);
|
||||
|
||||
if (!collection.private) {
|
||||
throw new InvalidRequestError('Collection must be private to remove users');
|
||||
}
|
||||
|
||||
const user = await User.findById(userId);
|
||||
authorize(ctx.state.user, 'read', user);
|
||||
|
||||
await collection.removeUser(user);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('collections.users', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const collection = await Collection.findById(id);
|
||||
authorize(ctx.state.user, 'read', collection);
|
||||
|
||||
const users = await collection.getUsers();
|
||||
|
||||
const data = await Promise.all(
|
||||
users.map(async user => await presentUser(ctx, user))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
data,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('collections.export', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.findById(id);
|
||||
@@ -78,16 +147,33 @@ router.post('collections.exportAll', auth(), async ctx => {
|
||||
|
||||
router.post('collections.update', auth(), async ctx => {
|
||||
const { id, name, description, color } = ctx.body;
|
||||
const isPrivate = ctx.body.private;
|
||||
|
||||
ctx.assertPresent(name, 'name is required');
|
||||
if (color)
|
||||
ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.findById(id);
|
||||
authorize(ctx.state.user, 'update', collection);
|
||||
authorize(user, 'update', collection);
|
||||
|
||||
if (isPrivate && !collection.private) {
|
||||
await CollectionUser.findOrCreate({
|
||||
where: {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
},
|
||||
defaults: {
|
||||
permission: 'read_write',
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
collection.name = name;
|
||||
collection.description = description;
|
||||
collection.color = color;
|
||||
collection.private = isPrivate;
|
||||
await collection.save();
|
||||
|
||||
ctx.body = {
|
||||
@@ -97,9 +183,12 @@ router.post('collections.update', auth(), async ctx => {
|
||||
|
||||
router.post('collections.list', auth(), pagination(), async ctx => {
|
||||
const user = ctx.state.user;
|
||||
const collections = await Collection.findAll({
|
||||
|
||||
const collectionIds = await user.collectionIds();
|
||||
let collections = await Collection.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
id: collectionIds,
|
||||
},
|
||||
order: [['updatedAt', 'DESC']],
|
||||
offset: ctx.state.pagination.offset,
|
||||
@@ -120,7 +209,7 @@ router.post('collections.list', auth(), pagination(), async ctx => {
|
||||
|
||||
router.post('collections.delete', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const collection = await Collection.findById(id);
|
||||
authorize(ctx.state.user, 'delete', collection);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import TestServer from 'fetch-test-server';
|
||||
import app from '..';
|
||||
import { flushdb, seed } from '../test/support';
|
||||
import { buildUser } from '../test/factories';
|
||||
import { buildUser, buildCollection } from '../test/factories';
|
||||
import { Collection } from '../models';
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
@@ -29,9 +29,54 @@ describe('#collections.list', async () => {
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(collection.id);
|
||||
});
|
||||
|
||||
it('should not return private collections not a member of', async () => {
|
||||
const { user, collection } = await seed();
|
||||
await buildCollection({
|
||||
private: true,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post('/api/collections.list', {
|
||||
body: { token: user.getJwtToken() },
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(collection.id);
|
||||
});
|
||||
|
||||
it('should return private collections member of', async () => {
|
||||
const { user } = await seed();
|
||||
await buildCollection({
|
||||
private: true,
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post('/api/collections.list', {
|
||||
body: { token: user.getJwtToken() },
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#collections.export', async () => {
|
||||
it('should require user to be a member', async () => {
|
||||
const { user } = await seed();
|
||||
const collection = await buildCollection({
|
||||
private: true,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post('/api/collections.export', {
|
||||
body: { token: user.getJwtToken(), id: collection.id },
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/collections.export');
|
||||
const body = await res.json();
|
||||
@@ -77,6 +122,170 @@ describe('#collections.exportAll', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#collections.add_user', async () => {
|
||||
it('should add user to collection', async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
private: true,
|
||||
});
|
||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||
const res = await server.post('/api/collections.add_user', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
const users = await collection.getUsers();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(users.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should require user in team', async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
private: true,
|
||||
});
|
||||
const anotherUser = await buildUser();
|
||||
const res = await server.post('/api/collections.add_user', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/collections.add_user');
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
|
||||
it('should require authorization', async () => {
|
||||
const { collection } = await seed();
|
||||
const user = await buildUser();
|
||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||
|
||||
const res = await server.post('/api/collections.add_user', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#collections.remove_user', async () => {
|
||||
it('should remove user from collection', async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
private: true,
|
||||
});
|
||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||
|
||||
await server.post('/api/collections.add_user', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await server.post('/api/collections.remove_user', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
const users = await collection.getUsers();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(users.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should require user in team', async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
private: true,
|
||||
});
|
||||
const anotherUser = await buildUser();
|
||||
const res = await server.post('/api/collections.remove_user', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/collections.remove_user');
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
|
||||
it('should require authorization', async () => {
|
||||
const { collection } = await seed();
|
||||
const user = await buildUser();
|
||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||
|
||||
const res = await server.post('/api/collections.remove_user', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#collections.users', async () => {
|
||||
it('should return members in private collection', async () => {
|
||||
const { collection, user } = await seed();
|
||||
const res = await server.post('/api/collections.users', {
|
||||
body: { token: user.getJwtToken(), id: collection.id },
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/collections.users');
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should require authorization', async () => {
|
||||
const { collection } = await seed();
|
||||
const user = await buildUser();
|
||||
const res = await server.post('/api/collections.users', {
|
||||
body: { token: user.getJwtToken(), id: collection.id },
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#collections.info', async () => {
|
||||
it('should return collection', async () => {
|
||||
const { user, collection } = await seed();
|
||||
@@ -89,6 +298,17 @@ describe('#collections.info', async () => {
|
||||
expect(body.data.id).toEqual(collection.id);
|
||||
});
|
||||
|
||||
it('should require user member of collection', async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post('/api/collections.info', {
|
||||
body: { token: user.getJwtToken(), id: collection.id },
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/collections.info');
|
||||
const body = await res.json();
|
||||
|
||||
@@ -14,14 +14,39 @@ const { authorize, cannot } = policy;
|
||||
const router = new Router();
|
||||
|
||||
router.post('documents.list', auth(), pagination(), async ctx => {
|
||||
let { sort = 'updatedAt', direction, collection, user } = ctx.body;
|
||||
const { sort = 'updatedAt' } = ctx.body;
|
||||
const collectionId = ctx.body.collection;
|
||||
const createdById = ctx.body.user;
|
||||
let direction = ctx.body.direction;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
|
||||
let where = { teamId: ctx.state.user.teamId };
|
||||
if (collection) where = { ...where, collectionId: collection };
|
||||
if (user) where = { ...where, createdById: user };
|
||||
// always filter by the current team
|
||||
const user = ctx.state.user;
|
||||
let where = { teamId: user.teamId };
|
||||
|
||||
const starredScope = { method: ['withStarred', ctx.state.user.id] };
|
||||
// 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');
|
||||
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');
|
||||
|
||||
where = { ...where, collectionId };
|
||||
const collection = await Collection.findById(collectionId);
|
||||
authorize(user, 'read', collection);
|
||||
|
||||
// otherwise, filter by all collections the user has access to
|
||||
} else {
|
||||
const collectionIds = await user.collectionIds();
|
||||
where = { ...where, collectionId: collectionIds };
|
||||
}
|
||||
|
||||
// add the users starred state to the response by default
|
||||
const starredScope = { method: ['withStarred', user.id] };
|
||||
const documents = await Document.scope('defaultScope', starredScope).findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
@@ -40,16 +65,21 @@ router.post('documents.list', auth(), pagination(), async ctx => {
|
||||
});
|
||||
|
||||
router.post('documents.pinned', auth(), pagination(), async ctx => {
|
||||
let { sort = 'updatedAt', direction, collection } = ctx.body;
|
||||
const { sort = 'updatedAt' } = ctx.body;
|
||||
const collectionId = ctx.body.collection;
|
||||
let direction = ctx.body.direction;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
ctx.assertPresent(collection, 'collection is required');
|
||||
ctx.assertUuid(collectionId, 'collection is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.findById(collectionId);
|
||||
authorize(user, 'read', collection);
|
||||
|
||||
const starredScope = { method: ['withStarred', user.id] };
|
||||
const documents = await Document.scope('defaultScope', starredScope).findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
collectionId: collection,
|
||||
collectionId,
|
||||
pinnedById: {
|
||||
// $FlowFixMe
|
||||
[Op.ne]: null,
|
||||
@@ -75,6 +105,8 @@ router.post('documents.viewed', auth(), pagination(), async ctx => {
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collectionIds = await user.collectionIds();
|
||||
|
||||
const views = await View.findAll({
|
||||
where: { userId: user.id },
|
||||
order: [[sort, direction]],
|
||||
@@ -82,6 +114,9 @@ router.post('documents.viewed', auth(), pagination(), async ctx => {
|
||||
{
|
||||
model: Document,
|
||||
required: true,
|
||||
where: {
|
||||
collectionId: collectionIds,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Star,
|
||||
@@ -111,13 +146,28 @@ router.post('documents.starred', auth(), pagination(), async ctx => {
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collectionIds = await user.collectionIds();
|
||||
|
||||
const views = await Star.findAll({
|
||||
where: { userId: user.id },
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
include: [
|
||||
{
|
||||
model: Document,
|
||||
include: [{ model: Star, as: 'starred', where: { userId: user.id } }],
|
||||
where: {
|
||||
collectionId: collectionIds,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Star,
|
||||
as: 'starred',
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
offset: ctx.state.pagination.offset,
|
||||
@@ -139,9 +189,15 @@ router.post('documents.drafts', auth(), pagination(), async ctx => {
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collectionIds = await user.collectionIds();
|
||||
|
||||
const documents = await Document.findAll({
|
||||
// $FlowFixMe
|
||||
where: { userId: user.id, publishedAt: { [Op.eq]: null } },
|
||||
where: {
|
||||
userId: user.id,
|
||||
collectionId: collectionIds,
|
||||
// $FlowFixMe
|
||||
publishedAt: { [Op.eq]: null },
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
@@ -199,6 +255,7 @@ router.post('documents.revision', auth(), async ctx => {
|
||||
let { id, revisionId } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertPresent(revisionId, 'revisionId is required');
|
||||
|
||||
const document = await Document.findById(id);
|
||||
authorize(ctx.state.user, 'read', document);
|
||||
|
||||
@@ -346,7 +403,6 @@ router.post('documents.unstar', auth(), async ctx => {
|
||||
router.post('documents.create', auth(), async ctx => {
|
||||
const { title, text, publish, parentDocument, index } = ctx.body;
|
||||
const collectionId = ctx.body.collection;
|
||||
ctx.assertPresent(collectionId, 'collection is required');
|
||||
ctx.assertUuid(collectionId, 'collection must be an uuid');
|
||||
ctx.assertPresent(title, 'title is required');
|
||||
ctx.assertPresent(text, 'text is required');
|
||||
|
||||
@@ -3,7 +3,12 @@ import TestServer from 'fetch-test-server';
|
||||
import app from '..';
|
||||
import { Document, View, Star, Revision } from '../models';
|
||||
import { flushdb, seed } from '../test/support';
|
||||
import { buildShare, buildUser, buildDocument } from '../test/factories';
|
||||
import {
|
||||
buildShare,
|
||||
buildCollection,
|
||||
buildUser,
|
||||
buildDocument,
|
||||
} from '../test/factories';
|
||||
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
@@ -22,6 +27,18 @@ describe('#documents.info', async () => {
|
||||
expect(body.data.id).toEqual(document.id);
|
||||
});
|
||||
|
||||
it('should not return published 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/documents.info', {
|
||||
body: { token: user.getJwtToken(), id: document.id },
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it('should return drafts', async () => {
|
||||
const { user, document } = await seed();
|
||||
document.publishedAt = null;
|
||||
@@ -36,7 +53,7 @@ describe('#documents.info', async () => {
|
||||
expect(body.data.id).toEqual(document.id);
|
||||
});
|
||||
|
||||
it('should return redacted document from shareId without token', async () => {
|
||||
it('should return document from shareId without token', async () => {
|
||||
const { document } = await seed();
|
||||
const share = await buildShare({
|
||||
documentId: document.id,
|
||||
@@ -141,6 +158,20 @@ describe('#documents.list', async () => {
|
||||
expect(body.data.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should not return documents in private collections not a member of', async () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.private = true;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post('/api/documents.list', {
|
||||
body: { token: user.getJwtToken() },
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should allow changing sort direction', async () => {
|
||||
const { user, document } = await seed();
|
||||
const res = await server.post('/api/documents.list', {
|
||||
@@ -189,6 +220,23 @@ describe('#documents.drafts', async () => {
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should not return documents in private collections not a member of', async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
document.publishedAt = null;
|
||||
await document.save();
|
||||
|
||||
collection.private = true;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post('/api/documents.drafts', {
|
||||
body: { token: user.getJwtToken() },
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#documents.revision', async () => {
|
||||
@@ -208,6 +256,18 @@ describe('#documents.revision', async () => {
|
||||
expect(body.data[0].title).toEqual(document.title);
|
||||
});
|
||||
|
||||
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/documents.revisions', {
|
||||
body: { token: user.getJwtToken(), id: document.id },
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it('should require authorization', async () => {
|
||||
const { document } = await seed();
|
||||
const user = await buildUser();
|
||||
@@ -296,6 +356,26 @@ describe('#documents.search', async () => {
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not return documents in private collections not a member of', async () => {
|
||||
const { user } = await seed();
|
||||
const collection = await buildCollection({ private: true });
|
||||
|
||||
await buildDocument({
|
||||
title: 'search term',
|
||||
text: 'search term',
|
||||
publishedAt: null,
|
||||
teamId: user.teamId,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
const res = await server.post('/api/documents.search', {
|
||||
body: { token: user.getJwtToken(), query: 'search term' },
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/documents.search');
|
||||
const body = await res.json();
|
||||
@@ -345,6 +425,21 @@ describe('#documents.viewed', async () => {
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not return recently viewed documents in collection not a member of', async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
await View.increment({ documentId: document.id, userId: user.id });
|
||||
collection.private = true;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post('/api/documents.viewed', {
|
||||
body: { token: user.getJwtToken() },
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/documents.viewed');
|
||||
const body = await res.json();
|
||||
|
||||
@@ -33,7 +33,7 @@ router.post('integrations.list', auth(), pagination(), async ctx => {
|
||||
|
||||
router.post('integrations.delete', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const integration = await Integration.findById(id);
|
||||
authorize(ctx.state.user, 'delete', integration);
|
||||
|
||||
@@ -44,7 +44,7 @@ router.post('notificationSettings.list', auth(), async ctx => {
|
||||
|
||||
router.post('notificationSettings.delete', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const setting = await NotificationSetting.findById(id);
|
||||
@@ -59,7 +59,7 @@ router.post('notificationSettings.delete', auth(), async ctx => {
|
||||
|
||||
router.post('notificationSettings.unsubscribe', async ctx => {
|
||||
const { id, token } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
ctx.assertPresent(token, 'token is required');
|
||||
|
||||
const setting = await NotificationSetting.findById(id);
|
||||
|
||||
@@ -80,7 +80,7 @@ router.post('shares.create', auth(), async ctx => {
|
||||
|
||||
router.post('shares.revoke', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const share = await Share.findById(id);
|
||||
|
||||
@@ -10,7 +10,7 @@ const router = new Router();
|
||||
|
||||
router.post('views.list', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
@@ -40,7 +40,7 @@ router.post('views.list', auth(), async ctx => {
|
||||
|
||||
router.post('views.create', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
Reference in New Issue
Block a user