feat: Memberships (#1032)

* WIP

* feat: Add collection.memberships endpoint

* feat: Add ability to filter collection.memberships with query

* WIP

* Merge stashed work

* feat: Add ability to filter memberships by permission

* continued refactoring

* paginated list component

* Collection member management

* fix: Incorrect policy data sent down after collection.update

* Reduce duplication, add empty state

* cleanup

* fix: Modal close should be a real button

* fix: Allow opening edit from modal

* fix: remove unused methods

* test: fix

* Passing test suite

* Refactor

* fix: Flow UI errors

* test: Add collections.update tests

* lint

* test: moar tests

* fix: Missing scopes, more missing tests

* fix: Handle collection privacy change over socket

* fix: More membership scopes

* fix: view endpoint permissions

* fix: respond to privacy change on socket event

* policy driven menus

* fix: share endpoint policies

* chore: Use policies to drive documents UI

* alignment

* fix: Header height

* fix: Correct behavior when collection becomes private

* fix: Header height for read-only collection

* send id's over socket instead of serialized objects

* fix: Remote policy change

* fix: reduce collection fetching

* More websocket efficiencies

* fix: Document collection pinning

* fix: Restored ability to edit drafts
fix: Removed ability to star drafts

* fix: Require write permissions to pin doc to collection

* fix: Header title overlaying document actions at small screen sizes

* fix: Jank on load caused by previous commit

* fix: Double collection fetch post-publish

* fix: Hide publish button if draft is in no longer accessible collection

* fix: Always allow deleting drafts
fix: Improved handling of deleted documents

* feat: Show collections in drafts view
feat: Show more obvious 'draft' badge on documents

* fix: incorrect policies after publish to private collection

* fix: Duplicating a draft publishes it
This commit is contained in:
Tom Moor
2019-10-05 18:42:03 -07:00
committed by GitHub
parent 4164fc178c
commit b42e9737b6
72 changed files with 2360 additions and 765 deletions

View File

@@ -53,7 +53,9 @@ router.post('documents.list', auth(), pagination(), async ctx => {
ctx.assertUuid(collectionId, 'collection must be a UUID');
where = { ...where, collectionId };
const collection = await Collection.findByPk(collectionId);
const collection = await Collection.scope({
method: ['withMembership', user.id],
}).findByPk(collectionId);
authorize(user, 'read', collection);
// otherwise, filter by all collections the user has access to
@@ -78,7 +80,12 @@ 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 documents = await Document.scope('defaultScope', starredScope).findAll({
const collectionScope = { method: ['withCollection', user.id] };
const documents = await Document.scope(
'defaultScope',
starredScope,
collectionScope
).findAll({
where,
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
@@ -99,18 +106,24 @@ router.post('documents.list', auth(), pagination(), async ctx => {
});
router.post('documents.pinned', auth(), pagination(), async ctx => {
const { sort = 'updatedAt' } = ctx.body;
const collectionId = ctx.body.collection;
const { collectionId, sort = 'updatedAt' } = ctx.body;
let direction = ctx.body.direction;
if (direction !== 'ASC') direction = 'DESC';
ctx.assertUuid(collectionId, 'collection is required');
ctx.assertUuid(collectionId, 'collectionId is required');
const user = ctx.state.user;
const collection = await Collection.findByPk(collectionId);
const collection = await Collection.scope({
method: ['withMembership', user.id],
}).findByPk(collectionId);
authorize(user, 'read', collection);
const starredScope = { method: ['withStarred', user.id] };
const documents = await Document.scope('defaultScope', starredScope).findAll({
const collectionScope = { method: ['withCollection', user.id] };
const documents = await Document.scope(
'defaultScope',
starredScope,
collectionScope
).findAll({
where: {
teamId: user.teamId,
collectionId,
@@ -269,7 +282,11 @@ router.post('documents.drafts', auth(), pagination(), async ctx => {
const user = ctx.state.user;
const collectionIds = await user.collectionIds();
const documents = await Document.findAll({
const collectionScope = { method: ['withCollection', user.id] };
const documents = await Document.scope(
'defaultScope',
collectionScope
).findAll({
where: {
userId: user.id,
collectionId: collectionIds,
@@ -324,7 +341,10 @@ router.post('documents.info', auth({ required: false }), async ctx => {
}
document = share.document;
} else {
document = await Document.findByPk(id);
document = await Document.findByPk(
id,
user ? { userId: user.id } : undefined
);
authorize(user, 'read', document);
}
@@ -341,8 +361,9 @@ router.post('documents.revision', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(revisionId, 'revisionId is required');
const document = await Document.findByPk(id);
authorize(ctx.state.user, 'read', document);
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'read', document);
const revision = await Revision.findOne({
where: {
@@ -361,9 +382,10 @@ router.post('documents.revisions', auth(), pagination(), async ctx => {
let { id, sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
ctx.assertPresent(id, 'id is required');
const document = await Document.findByPk(id);
authorize(ctx.state.user, 'read', document);
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'read', document);
const revisions = await Revision.findAll({
where: { documentId: id },
@@ -383,7 +405,7 @@ router.post('documents.restore', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findByPk(id);
const document = await Document.findByPk(id, { userId: user.id });
if (document.archivedAt) {
authorize(user, 'unarchive', document);
@@ -439,7 +461,9 @@ router.post('documents.search', auth(), pagination(), async ctx => {
if (collectionId) {
ctx.assertUuid(collectionId, 'collectionId must be a UUID');
const collection = await Collection.findByPk(collectionId);
const collection = await Collection.scope({
method: ['withMembership', user.id],
}).findByPk(collectionId);
authorize(user, 'read', collection);
}
@@ -486,10 +510,10 @@ router.post('documents.search', auth(), pagination(), async ctx => {
router.post('documents.pin', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findByPk(id);
authorize(user, 'update', document);
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'pin', document);
document.pinnedById = user.id;
await document.save();
@@ -513,10 +537,10 @@ router.post('documents.pin', auth(), async ctx => {
router.post('documents.unpin', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findByPk(id);
authorize(user, 'update', document);
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'unpin', document);
document.pinnedById = null;
await document.save();
@@ -540,9 +564,9 @@ router.post('documents.unpin', auth(), async ctx => {
router.post('documents.star', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findByPk(id);
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'read', document);
await Star.findOrCreate({
@@ -563,9 +587,9 @@ router.post('documents.star', auth(), async ctx => {
router.post('documents.unstar', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findByPk(id);
const user = ctx.state.user;
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'read', document);
await Star.destroy({
@@ -574,7 +598,7 @@ router.post('documents.unstar', auth(), async ctx => {
await Event.create({
name: 'documents.unstar',
modelId: document.id,
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
actorId: user.id,
@@ -602,7 +626,9 @@ router.post('documents.create', auth(), async ctx => {
const user = ctx.state.user;
authorize(user, 'create', Document);
const collection = await Collection.findOne({
const collection = await Collection.scope({
method: ['withMembership', user.id],
}).findOne({
where: {
id: collectionId,
teamId: user.teamId,
@@ -618,7 +644,7 @@ router.post('documents.create', auth(), async ctx => {
collectionId: collection.id,
},
});
authorize(user, 'read', parentDocument);
authorize(user, 'read', parentDocument, { collection });
}
let document = await Document.create({
@@ -662,6 +688,7 @@ router.post('documents.create', auth(), async ctx => {
document = await Document.findOne({
where: { id: document.id, publishedAt: document.publishedAt },
});
document.collection = collection;
ctx.body = {
data: await presentDocument(document),
@@ -685,9 +712,8 @@ router.post('documents.update', auth(), async ctx => {
if (append) ctx.assertPresent(text, 'Text is required while appending');
const user = ctx.state.user;
const document = await Document.findByPk(id);
authorize(ctx.state.user, 'update', document);
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'update', document);
if (lastRevision && lastRevision !== document.revisionCount) {
throw new InvalidRequestError('Document has changed since last revision');
@@ -702,6 +728,7 @@ router.post('documents.update', auth(), async ctx => {
document.text = text;
}
document.lastModifiedById = user.id;
const { collection } = document;
let transaction;
try {
@@ -746,6 +773,8 @@ router.post('documents.update', auth(), async ctx => {
});
}
document.collection = collection;
ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),
@@ -770,12 +799,10 @@ router.post('documents.move', auth(), async ctx => {
}
const user = ctx.state.user;
const document = await Document.findByPk(id);
const document = await Document.findByPk(id, { userId: user.id });
const { collection } = document;
authorize(user, 'move', document);
const collection = await Collection.findByPk(collectionId);
authorize(user, 'update', collection);
if (collection.type !== 'atlas' && parentDocumentId) {
throw new InvalidRequestError(
'Document cannot be nested in this collection type'
@@ -783,7 +810,7 @@ router.post('documents.move', auth(), async ctx => {
}
if (parentDocumentId) {
const parent = await Document.findByPk(parentDocumentId);
const parent = await Document.findByPk(parentDocumentId, user.id);
authorize(user, 'update', parent);
}
@@ -814,7 +841,7 @@ router.post('documents.archive', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findByPk(id);
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'archive', document);
await document.archive(user.id);
@@ -840,7 +867,7 @@ router.post('documents.delete', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findByPk(id);
const document = await Document.findByPk(id, { userId: user.id });
authorize(user, 'delete', document);
await document.delete();