feat: Collection admins (#5273
* Split permissions for reading documents from updating collection * fix: Admins should have collection read permission, tests * tsc * Add admin option to permission selector * Combine publish and create permissions, update -> createDocuments where appropriate * Plural -> singular * wip * Quick version of collection structure loading, will revisit * Remove documentIds method * stash * fixing tests to account for admin creation * Add self-hosted migration * fix: Allow groups to have admin permission * Prefetch collection documents * fix: Document explorer (move/publish) not working with async documents * fix: Cannot re-parent document to collection by drag and drop * fix: Cannot drag to import into collection item without admin permission * Remove unused isEditor getter
This commit is contained in:
56
server/migrations/20230429005039-collection-admins.js
Normal file
56
server/migrations/20230429005039-collection-admins.js
Normal file
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
if (process.env.DEPLOYMENT === "hosted") {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
// Convert collection members to admins where the user is the only
|
||||
// membership in the collection.
|
||||
await queryInterface.sequelize.query(
|
||||
`UPDATE collection_users cu
|
||||
SET permission = 'admin'
|
||||
WHERE (
|
||||
SELECT COUNT(*)
|
||||
FROM collection_users
|
||||
WHERE "collectionId" = cu."collectionId"
|
||||
AND permission = 'read_write'
|
||||
) = 1;`,
|
||||
{
|
||||
type: queryInterface.sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
);
|
||||
|
||||
// Convert collection members to admins where the collection is private
|
||||
// and they currently have read_write permission
|
||||
await queryInterface.sequelize.query(
|
||||
`UPDATE collection_users
|
||||
SET permission = 'admin'
|
||||
WHERE permission = 'read_write'
|
||||
AND "collectionId" IN (
|
||||
SELECT c."id"
|
||||
FROM collections c
|
||||
WHERE c.permission IS NULL
|
||||
);`,
|
||||
{
|
||||
type: queryInterface.sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
if (process.env.DEPLOYMENT === "hosted") {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
"UPDATE collection_users SET permission = 'read_write' WHERE permission = 'admin'",
|
||||
{
|
||||
type: queryInterface.sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -254,21 +254,17 @@ class Collection extends ParanoidModel {
|
||||
model: Collection,
|
||||
options: { transaction: Transaction }
|
||||
) {
|
||||
if (model.permission !== CollectionPermission.ReadWrite) {
|
||||
return CollectionUser.findOrCreate({
|
||||
where: {
|
||||
collectionId: model.id,
|
||||
userId: model.createdById,
|
||||
},
|
||||
defaults: {
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
createdById: model.createdById,
|
||||
},
|
||||
transaction: options.transaction,
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return CollectionUser.findOrCreate({
|
||||
where: {
|
||||
collectionId: model.id,
|
||||
userId: model.createdById,
|
||||
},
|
||||
defaults: {
|
||||
permission: CollectionPermission.Admin,
|
||||
createdById: model.createdById,
|
||||
},
|
||||
transaction: options.transaction,
|
||||
});
|
||||
}
|
||||
|
||||
// associations
|
||||
@@ -396,6 +392,17 @@ class Collection extends ParanoidModel {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to return if a collection is considered private.
|
||||
* This means that a membership is required to view it rather than just being
|
||||
* a workspace member.
|
||||
*
|
||||
* @returns boolean
|
||||
*/
|
||||
get isPrivate() {
|
||||
return !this.permission;
|
||||
}
|
||||
|
||||
getDocumentTree = (documentId: string): NavigationNode | null => {
|
||||
if (!this.documentStructure) {
|
||||
return null;
|
||||
|
||||
@@ -473,6 +473,25 @@ class Document extends ParanoidModel {
|
||||
|
||||
// instance methods
|
||||
|
||||
/**
|
||||
* Whether this document is considered active or not. A document is active if
|
||||
* it has not been archived or deleted.
|
||||
*
|
||||
* @returns boolean
|
||||
*/
|
||||
get isActive(): boolean {
|
||||
return !this.archivedAt && !this.deletedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method that returns whether this document is a draft.
|
||||
*
|
||||
* @returns boolean
|
||||
*/
|
||||
get isDraft(): boolean {
|
||||
return !this.publishedAt;
|
||||
}
|
||||
|
||||
get titleWithDefault(): string {
|
||||
return this.title || "Untitled";
|
||||
}
|
||||
|
||||
@@ -380,8 +380,9 @@ class User extends ParanoidModel {
|
||||
return collectionStubs
|
||||
.filter(
|
||||
(c) =>
|
||||
c.permission === CollectionPermission.Read ||
|
||||
c.permission === CollectionPermission.ReadWrite ||
|
||||
Object.values(CollectionPermission).includes(
|
||||
c.permission as CollectionPermission
|
||||
) ||
|
||||
c.memberships.length > 0 ||
|
||||
c.collectionGroupMemberships.length > 0
|
||||
)
|
||||
|
||||
@@ -1,14 +1,71 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { CollectionUser, Collection } from "@server/models";
|
||||
import { buildUser, buildTeam, buildCollection } from "@server/test/factories";
|
||||
import {
|
||||
buildUser,
|
||||
buildTeam,
|
||||
buildCollection,
|
||||
buildAdmin,
|
||||
} from "@server/test/factories";
|
||||
import { setupTestDatabase } from "@server/test/support";
|
||||
import { serialize } from "./index";
|
||||
|
||||
setupTestDatabase();
|
||||
|
||||
describe("admin", () => {
|
||||
it("should allow updating collection but not reading documents", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
// reload to get membership
|
||||
const reloaded = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collection.id);
|
||||
const abilities = serialize(user, reloaded);
|
||||
expect(abilities.readDocument).toEqual(false);
|
||||
expect(abilities.createDocument).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("member", () => {
|
||||
describe("admin permission", () => {
|
||||
it("should allow updating collection", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.Admin,
|
||||
});
|
||||
// reload to get membership
|
||||
const reloaded = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collection.id);
|
||||
const abilities = serialize(user, reloaded);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.createDocument).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("read_write permission", () => {
|
||||
it("should allow read write permissions for team member", async () => {
|
||||
it("should allow read write documents for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
@@ -19,8 +76,9 @@ describe("member", () => {
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
});
|
||||
|
||||
it("should override read membership permission", async () => {
|
||||
@@ -44,8 +102,9 @@ describe("member", () => {
|
||||
}).findByPk(collection.id);
|
||||
const abilities = serialize(user, reloaded);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,8 +145,9 @@ describe("member", () => {
|
||||
}).findByPk(collection.id);
|
||||
const abilities = serialize(user, reloaded);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,8 +163,10 @@ describe("member", () => {
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(false);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.readDocument).toEqual(false);
|
||||
expect(abilities.createDocument).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
expect(abilities.update).toEqual(false);
|
||||
});
|
||||
|
||||
it("should allow override with team member membership permission", async () => {
|
||||
@@ -128,8 +190,10 @@ describe("member", () => {
|
||||
}).findByPk(collection.id);
|
||||
const abilities = serialize(user, reloaded);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.createDocument).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -148,6 +212,8 @@ describe("viewer", () => {
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.createDocument).toEqual(false);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
});
|
||||
@@ -174,8 +240,9 @@ describe("viewer", () => {
|
||||
}).findByPk(collection.id);
|
||||
const abilities = serialize(user, reloaded);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -202,8 +269,10 @@ describe("viewer", () => {
|
||||
}).findByPk(collection.id);
|
||||
const abilities = serialize(user, reloaded);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.createDocument).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,8 +315,10 @@ describe("viewer", () => {
|
||||
}).findByPk(collection.id);
|
||||
const abilities = serialize(user, reloaded);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(true);
|
||||
expect(abilities.readDocument).toEqual(true);
|
||||
expect(abilities.createDocument).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,26 +40,29 @@ allow(User, "move", Collection, (user, collection) => {
|
||||
throw AdminRequiredError();
|
||||
});
|
||||
|
||||
allow(User, ["read", "star", "unstar"], Collection, (user, collection) => {
|
||||
allow(User, "read", Collection, (user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!collection.permission) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
);
|
||||
const allMemberships = [
|
||||
...collection.memberships,
|
||||
...collection.collectionGroupMemberships,
|
||||
];
|
||||
return some(allMemberships, (m) =>
|
||||
Object.values(CollectionPermission).includes(m.permission)
|
||||
);
|
||||
if (collection.isPrivate) {
|
||||
return includesMembership(collection, Object.values(CollectionPermission));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
allow(User, ["star", "unstar"], Collection, (user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (collection.isPrivate) {
|
||||
return includesMembership(collection, Object.values(CollectionPermission));
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -72,32 +75,56 @@ allow(User, "share", Collection, (user, collection) => {
|
||||
if (!collection.sharing) {
|
||||
return false;
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
collection.permission !== CollectionPermission.ReadWrite ||
|
||||
user.isViewer
|
||||
) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
);
|
||||
const allMemberships = [
|
||||
...collection.memberships,
|
||||
...collection.collectionGroupMemberships,
|
||||
];
|
||||
return some(
|
||||
allMemberships,
|
||||
(m) => m.permission === CollectionPermission.ReadWrite
|
||||
);
|
||||
return includesMembership(collection, [
|
||||
CollectionPermission.ReadWrite,
|
||||
CollectionPermission.Admin,
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
allow(User, ["publish", "update"], Collection, (user, collection) => {
|
||||
allow(User, "readDocument", Collection, (user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (collection.isPrivate) {
|
||||
return includesMembership(collection, Object.values(CollectionPermission));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
allow(
|
||||
User,
|
||||
["updateDocument", "createDocument", "deleteDocument"],
|
||||
Collection,
|
||||
(user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
collection.permission !== CollectionPermission.ReadWrite ||
|
||||
user.isViewer
|
||||
) {
|
||||
return includesMembership(collection, [
|
||||
CollectionPermission.ReadWrite,
|
||||
CollectionPermission.Admin,
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
allow(User, ["update", "delete"], Collection, (user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
@@ -105,56 +132,19 @@ allow(User, ["publish", "update"], Collection, (user, collection) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
collection.permission !== CollectionPermission.ReadWrite ||
|
||||
user.isViewer
|
||||
) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
);
|
||||
const allMemberships = [
|
||||
...collection.memberships,
|
||||
...collection.collectionGroupMemberships,
|
||||
];
|
||||
return some(
|
||||
allMemberships,
|
||||
(m) => m.permission === CollectionPermission.ReadWrite
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
return includesMembership(collection, [CollectionPermission.Admin]);
|
||||
});
|
||||
|
||||
allow(User, "delete", Collection, (user, collection) => {
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
collection.permission !== CollectionPermission.ReadWrite ||
|
||||
user.isViewer
|
||||
) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
);
|
||||
const allMemberships = [
|
||||
...collection.memberships,
|
||||
...collection.collectionGroupMemberships,
|
||||
];
|
||||
return some(
|
||||
allMemberships,
|
||||
(m) => m.permission === CollectionPermission.ReadWrite
|
||||
);
|
||||
}
|
||||
|
||||
if (user.id === collection.createdById) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw AdminRequiredError();
|
||||
});
|
||||
function includesMembership(
|
||||
collection: Collection,
|
||||
memberships: CollectionPermission[]
|
||||
) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"memberships should be preloaded, did you forget withMembership scope?"
|
||||
);
|
||||
return some(
|
||||
[...collection.memberships, ...collection.collectionGroupMemberships],
|
||||
(m) => memberships.includes(m.permission)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ allow(User, ["read", "comment"], Document, (user, document) => {
|
||||
}
|
||||
|
||||
// existence of collection option is not required here to account for share tokens
|
||||
if (document.collection && cannot(user, "read", document.collection)) {
|
||||
if (
|
||||
document.collection &&
|
||||
cannot(user, "readDocument", document.collection)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -29,7 +32,10 @@ allow(User, "download", Document, (user, document) => {
|
||||
}
|
||||
|
||||
// existence of collection option is not required here to account for share tokens
|
||||
if (document.collection && cannot(user, "read", document.collection)) {
|
||||
if (
|
||||
document.collection &&
|
||||
cannot(user, "readDocument", document.collection)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -44,16 +50,7 @@ allow(User, "download", Document, (user, document) => {
|
||||
});
|
||||
|
||||
allow(User, "star", Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.template) {
|
||||
if (!document || !document.isActive || document.template) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -62,7 +59,7 @@ allow(User, "star", Document, (user, document) => {
|
||||
document.collection,
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
);
|
||||
if (cannot(user, "read", document.collection)) {
|
||||
if (cannot(user, "readDocument", document.collection)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -83,7 +80,7 @@ allow(User, "unstar", Document, (user, document) => {
|
||||
document.collection,
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
);
|
||||
if (cannot(user, "read", document.collection)) {
|
||||
if (cannot(user, "readDocument", document.collection)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -114,13 +111,7 @@ allow(User, "share", Document, (user, document) => {
|
||||
});
|
||||
|
||||
allow(User, "update", Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
if (!document || !document.isActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -130,7 +121,7 @@ allow(User, "update", Document, (user, document) => {
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
);
|
||||
|
||||
if (cannot(user, "update", document.collection)) {
|
||||
if (cannot(user, "updateDocument", document.collection)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -139,63 +130,42 @@ allow(User, "update", Document, (user, document) => {
|
||||
});
|
||||
|
||||
allow(User, "createChildDocument", Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
if (!document || !document.isActive || document.isDraft) {
|
||||
return false;
|
||||
}
|
||||
if (document.template) {
|
||||
return false;
|
||||
}
|
||||
if (!document.publishedAt) {
|
||||
return false;
|
||||
}
|
||||
invariant(
|
||||
document.collection,
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
);
|
||||
if (cannot(user, "update", document.collection)) {
|
||||
if (cannot(user, "updateDocument", document.collection)) {
|
||||
return false;
|
||||
}
|
||||
return user.teamId === document.teamId;
|
||||
});
|
||||
|
||||
allow(User, "move", Document, (user, document) => {
|
||||
if (!document) {
|
||||
if (!document || !document.isActive) {
|
||||
return false;
|
||||
}
|
||||
if (document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.collection && cannot(user, "update", document.collection)) {
|
||||
if (
|
||||
document.collection &&
|
||||
cannot(user, "updateDocument", document.collection)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return user.teamId === document.teamId;
|
||||
});
|
||||
|
||||
allow(User, ["pin", "unpin"], Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
if (!document || !document.isActive || document.isDraft) {
|
||||
return false;
|
||||
}
|
||||
if (document.template) {
|
||||
return false;
|
||||
}
|
||||
if (!document.publishedAt) {
|
||||
return false;
|
||||
}
|
||||
invariant(
|
||||
document.collection,
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
@@ -207,46 +177,30 @@ allow(User, ["pin", "unpin"], Document, (user, document) => {
|
||||
});
|
||||
|
||||
allow(User, ["subscribe", "unsubscribe"], Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
if (!document || !document.isActive || document.isDraft) {
|
||||
return false;
|
||||
}
|
||||
if (document.template) {
|
||||
return false;
|
||||
}
|
||||
if (!document.publishedAt) {
|
||||
return false;
|
||||
}
|
||||
invariant(
|
||||
document.collection,
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
);
|
||||
if (cannot(user, "read", document.collection)) {
|
||||
if (cannot(user, "readDocument", document.collection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return user.teamId === document.teamId;
|
||||
});
|
||||
|
||||
allow(User, ["pinToHome"], Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.template) {
|
||||
return false;
|
||||
}
|
||||
if (!document.publishedAt) {
|
||||
allow(User, "pinToHome", Document, (user, document) => {
|
||||
if (
|
||||
!document ||
|
||||
!document.isActive ||
|
||||
document.isDraft ||
|
||||
document.template
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -254,23 +208,23 @@ allow(User, ["pinToHome"], Document, (user, document) => {
|
||||
});
|
||||
|
||||
allow(User, "delete", Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
if (!document || document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// allow deleting document without a collection
|
||||
if (document.collection && cannot(user, "update", document.collection)) {
|
||||
if (
|
||||
document.collection &&
|
||||
cannot(user, "deleteDocument", document.collection)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// unpublished drafts can always be deleted
|
||||
// unpublished drafts can always be deleted by their owner
|
||||
if (
|
||||
!document.deletedAt &&
|
||||
!document.publishedAt &&
|
||||
user.teamId === document.teamId
|
||||
document.isDraft &&
|
||||
user.id === document.createdById
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -279,15 +233,15 @@ allow(User, "delete", Document, (user, document) => {
|
||||
});
|
||||
|
||||
allow(User, "permanentDelete", Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (!document.deletedAt) {
|
||||
if (!document || !document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// allow deleting document without a collection
|
||||
if (document.collection && cannot(user, "update", document.collection)) {
|
||||
if (
|
||||
document.collection &&
|
||||
cannot(user, "updateDocument", document.collection)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -295,14 +249,14 @@ allow(User, "permanentDelete", Document, (user, document) => {
|
||||
});
|
||||
|
||||
allow(User, "restore", Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (!document.deletedAt) {
|
||||
if (!document || !document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.collection && cannot(user, "update", document.collection)) {
|
||||
if (
|
||||
document.collection &&
|
||||
cannot(user, "updateDocument", document.collection)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -310,23 +264,14 @@ allow(User, "restore", Document, (user, document) => {
|
||||
});
|
||||
|
||||
allow(User, "archive", Document, (user, document) => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
if (!document.publishedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (document.deletedAt) {
|
||||
if (!document || !document.isActive || document.isDraft) {
|
||||
return false;
|
||||
}
|
||||
invariant(
|
||||
document.collection,
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
);
|
||||
if (cannot(user, "update", document.collection)) {
|
||||
if (cannot(user, "updateDocument", document.collection)) {
|
||||
return false;
|
||||
}
|
||||
return user.teamId === document.teamId;
|
||||
@@ -340,7 +285,7 @@ allow(User, "unarchive", Document, (user, document) => {
|
||||
document.collection,
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
);
|
||||
if (cannot(user, "update", document.collection)) {
|
||||
if (cannot(user, "updateDocument", document.collection)) {
|
||||
return false;
|
||||
}
|
||||
if (!document.archivedAt) {
|
||||
@@ -360,17 +305,14 @@ allow(
|
||||
);
|
||||
|
||||
allow(User, "unpublish", Document, (user, document) => {
|
||||
if (!document) {
|
||||
if (!document || !document.isActive || document.isDraft) {
|
||||
return false;
|
||||
}
|
||||
invariant(
|
||||
document.collection,
|
||||
"collection is missing, did you forget to include in the query scope?"
|
||||
);
|
||||
if (!document.publishedAt || !!document.deletedAt || !!document.archivedAt) {
|
||||
return false;
|
||||
}
|
||||
if (cannot(user, "update", document.collection)) {
|
||||
if (cannot(user, "updateDocument", document.collection)) {
|
||||
return false;
|
||||
}
|
||||
return user.teamId === document.teamId;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import Collection from "@server/models/Collection";
|
||||
|
||||
export default function presentCollection(collection: Collection) {
|
||||
@@ -10,12 +11,11 @@ export default function presentCollection(collection: Collection) {
|
||||
sort: collection.sort,
|
||||
icon: collection.icon,
|
||||
index: collection.index,
|
||||
color: collection.color || "#4E5C6E",
|
||||
color: collection.color || colorPalette[0],
|
||||
permission: collection.permission,
|
||||
sharing: collection.sharing,
|
||||
createdAt: collection.createdAt,
|
||||
updatedAt: collection.updatedAt,
|
||||
deletedAt: collection.deletedAt,
|
||||
documents: collection.documentStructure || [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,20 +170,30 @@ export default class WebsocketsProcessor {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
return socketio.to(`team-${collection.teamId}`).emit("entities", {
|
||||
event: event.name,
|
||||
collectionIds: [
|
||||
{
|
||||
id: collection.id,
|
||||
updatedAt: collection.updatedAt,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return socketio
|
||||
.to(
|
||||
collection.permission
|
||||
? `collection-${event.collectionId}`
|
||||
: `team-${collection.teamId}`
|
||||
)
|
||||
.emit(event.name, presentCollection(collection));
|
||||
}
|
||||
|
||||
case "collections.delete": {
|
||||
const collection = await Collection.findByPk(event.collectionId, {
|
||||
paranoid: false,
|
||||
});
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
return socketio
|
||||
.to(`collection-${event.collectionId}`)
|
||||
.to(
|
||||
collection.permission
|
||||
? `collection-${event.collectionId}`
|
||||
: `team-${collection.teamId}`
|
||||
)
|
||||
.emit(event.name, {
|
||||
modelId: event.collectionId,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ export default class CollectionCreatedNotificationsTask extends BaseTask<
|
||||
const collection = await Collection.findByPk(event.collectionId);
|
||||
|
||||
// We only send notifications for collections visible to the entire team
|
||||
if (!collection || !collection.permission) {
|
||||
if (!collection || collection.isPrivate) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class ExportJSONTask extends ExportTask {
|
||||
private async addCollectionToArchive(zip: JSZip, collection: Collection) {
|
||||
const output: CollectionJSONExport = {
|
||||
collection: {
|
||||
...omit(presentCollection(collection), ["url", "documents"]),
|
||||
...omit(presentCollection(collection), ["url"]),
|
||||
description: collection.description
|
||||
? parser.parse(collection.description)
|
||||
: null,
|
||||
|
||||
@@ -396,18 +396,18 @@ describe("#collections.export_all", () => {
|
||||
|
||||
describe("#collections.add_user", () => {
|
||||
it("should add user to collection", async () => {
|
||||
const user = await buildUser();
|
||||
const admin = await buildAdmin();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
teamId: admin.teamId,
|
||||
userId: admin.id,
|
||||
permission: null,
|
||||
});
|
||||
const anotherUser = await buildUser({
|
||||
teamId: user.teamId,
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
const res = await server.post("/api/collections.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
@@ -616,25 +616,25 @@ describe("#collections.remove_group", () => {
|
||||
|
||||
describe("#collections.remove_user", () => {
|
||||
it("should remove user from collection", async () => {
|
||||
const user = await buildUser();
|
||||
const admin = await buildAdmin();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
teamId: admin.teamId,
|
||||
userId: admin.id,
|
||||
permission: null,
|
||||
});
|
||||
const anotherUser = await buildUser({
|
||||
teamId: user.teamId,
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
await server.post("/api/collections.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/collections.remove_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
@@ -839,12 +839,7 @@ describe("#collections.memberships", () => {
|
||||
const { collection, user } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/collections.memberships", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -857,7 +852,7 @@ describe("#collections.memberships", () => {
|
||||
expect(body.data.users[0].id).toEqual(user.id);
|
||||
expect(body.data.memberships.length).toEqual(1);
|
||||
expect(body.data.memberships[0].permission).toEqual(
|
||||
CollectionPermission.ReadWrite
|
||||
CollectionPermission.Admin
|
||||
);
|
||||
});
|
||||
|
||||
@@ -866,12 +861,6 @@ describe("#collections.memberships", () => {
|
||||
const user2 = await buildUser({
|
||||
name: "Won't find",
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user2.id,
|
||||
collectionId: collection.id,
|
||||
@@ -957,6 +946,12 @@ describe("#collections.info", () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.destroy({
|
||||
where: {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/collections.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -1181,10 +1176,10 @@ describe("#collections.update", () => {
|
||||
});
|
||||
|
||||
it("allows editing non-private collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
name: "Test",
|
||||
},
|
||||
@@ -1196,14 +1191,14 @@ describe("#collections.update", () => {
|
||||
});
|
||||
|
||||
it("allows editing sort", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
const sort = {
|
||||
field: "index",
|
||||
direction: "desc",
|
||||
};
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
sort,
|
||||
},
|
||||
@@ -1215,10 +1210,10 @@ describe("#collections.update", () => {
|
||||
});
|
||||
|
||||
it("allows editing individual fields", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
permission: null,
|
||||
},
|
||||
@@ -1230,10 +1225,10 @@ describe("#collections.update", () => {
|
||||
});
|
||||
|
||||
it("allows editing from non-private to private collection, and trims whitespace", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
permission: null,
|
||||
name: " Test ",
|
||||
@@ -1249,18 +1244,18 @@ describe("#collections.update", () => {
|
||||
});
|
||||
|
||||
it("allows editing from private to non-private collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
userId: admin.id,
|
||||
createdById: admin.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
name: "Test",
|
||||
@@ -1276,18 +1271,18 @@ describe("#collections.update", () => {
|
||||
});
|
||||
|
||||
it("allows editing by read-write collection user", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
userId: admin.id,
|
||||
createdById: admin.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
name: "Test",
|
||||
},
|
||||
@@ -1298,7 +1293,7 @@ describe("#collections.update", () => {
|
||||
expect(body.policies.length).toBe(1);
|
||||
});
|
||||
|
||||
it("allows editing by read-write collection group user", async () => {
|
||||
it("allows editing by admin collection group user", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
permission: null,
|
||||
@@ -1314,7 +1309,7 @@ describe("#collections.update", () => {
|
||||
});
|
||||
await collection.$add("group", group, {
|
||||
through: {
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
permission: CollectionPermission.Admin,
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
@@ -1335,12 +1330,18 @@ describe("#collections.update", () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
await CollectionUser.update(
|
||||
{
|
||||
createdById: user.id,
|
||||
permission: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -1352,14 +1353,14 @@ describe("#collections.update", () => {
|
||||
});
|
||||
|
||||
it("does not allow setting unknown sort fields", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
const sort = {
|
||||
field: "blah",
|
||||
direction: "desc",
|
||||
};
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
sort,
|
||||
},
|
||||
@@ -1368,14 +1369,14 @@ describe("#collections.update", () => {
|
||||
});
|
||||
|
||||
it("does not allow setting unknown sort directions", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
const sort = {
|
||||
field: "title",
|
||||
direction: "blah",
|
||||
};
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
sort,
|
||||
},
|
||||
@@ -1405,10 +1406,10 @@ describe("#collections.delete", () => {
|
||||
});
|
||||
|
||||
it("should not allow deleting last collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
const res = await server.post("/api/collections.delete", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
},
|
||||
});
|
||||
@@ -1416,15 +1417,15 @@ describe("#collections.delete", () => {
|
||||
});
|
||||
|
||||
it("should delete collection", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
// to ensure it isn't the last collection
|
||||
await buildCollection({
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
teamId: admin.teamId,
|
||||
createdById: admin.id,
|
||||
});
|
||||
const res = await server.post("/api/collections.delete", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
},
|
||||
});
|
||||
@@ -1434,11 +1435,11 @@ describe("#collections.delete", () => {
|
||||
});
|
||||
|
||||
it("should delete published documents", async () => {
|
||||
const { user, collection } = await seed();
|
||||
const { admin, collection } = await seed();
|
||||
// to ensure it isn't the last collection
|
||||
await buildCollection({
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
teamId: admin.teamId,
|
||||
createdById: admin.id,
|
||||
});
|
||||
// archived document should not be deleted
|
||||
await buildDocument({
|
||||
@@ -1447,7 +1448,7 @@ describe("#collections.delete", () => {
|
||||
});
|
||||
const res = await server.post("/api/collections.delete", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
token: admin.getJwtToken(),
|
||||
id: collection.id,
|
||||
},
|
||||
});
|
||||
@@ -1463,7 +1464,7 @@ describe("#collections.delete", () => {
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("allows deleting by read-write collection group user", async () => {
|
||||
it("allows deleting by admin collection group user", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
permission: null,
|
||||
@@ -1482,7 +1483,7 @@ describe("#collections.delete", () => {
|
||||
});
|
||||
await collection.$add("group", group, {
|
||||
through: {
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
permission: CollectionPermission.Admin,
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -148,6 +148,21 @@ router.post("collections.info", auth(), async (ctx: APIContext) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("collections.documents", auth(), async (ctx: APIContext) => {
|
||||
const { id } = ctx.request.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state.auth;
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
|
||||
authorize(user, "readDocument", collection);
|
||||
|
||||
ctx.body = {
|
||||
data: collection.documentStructure || [],
|
||||
};
|
||||
});
|
||||
|
||||
router.post(
|
||||
"collections.import",
|
||||
rateLimiter(RateLimiterStrategy.TenPerHour),
|
||||
@@ -641,7 +656,7 @@ router.post("collections.update", auth(), async (ctx: APIContext) => {
|
||||
authorize(user, "update", collection);
|
||||
|
||||
// we're making this collection have no default access, ensure that the
|
||||
// current user has a read-write membership so that at least they can edit it
|
||||
// current user has an admin membership so that at least they can manage it.
|
||||
if (
|
||||
permission !== CollectionPermission.ReadWrite &&
|
||||
collection.permission === CollectionPermission.ReadWrite
|
||||
@@ -652,7 +667,7 @@ router.post("collections.update", auth(), async (ctx: APIContext) => {
|
||||
userId: user.id,
|
||||
},
|
||||
defaults: {
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
permission: CollectionPermission.Admin,
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -695,6 +695,12 @@ describe("#documents.list", () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.destroy({
|
||||
where: {
|
||||
userId: user.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/documents.list", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -766,12 +772,18 @@ describe("#documents.list", () => {
|
||||
const { user, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
await CollectionUser.update(
|
||||
{
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
const res = await server.post("/api/documents.list", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -891,6 +903,12 @@ describe("#documents.drafts", () => {
|
||||
await document.save();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.destroy({
|
||||
where: {
|
||||
userId: user.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/documents.drafts", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -1792,6 +1810,12 @@ describe("#documents.viewed", () => {
|
||||
});
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.destroy({
|
||||
where: {
|
||||
userId: user.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/documents.viewed", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -2565,12 +2589,18 @@ describe("#documents.update", () => {
|
||||
await document.save();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionUser.update(
|
||||
{
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -2634,18 +2664,24 @@ describe("#documents.update", () => {
|
||||
});
|
||||
|
||||
it("allows editing by read-write collection user", async () => {
|
||||
const { admin, document, collection } = await seed();
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
collectionId: collection.id,
|
||||
userId: admin.id,
|
||||
createdById: admin.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionUser.update(
|
||||
{
|
||||
createdById: user.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
token: user.getJwtToken(),
|
||||
id: document.id,
|
||||
text: "Changed text",
|
||||
},
|
||||
@@ -2653,19 +2689,25 @@ describe("#documents.update", () => {
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.text).toBe("Changed text");
|
||||
expect(body.data.updatedBy.id).toBe(admin.id);
|
||||
expect(body.data.updatedBy.id).toBe(user.id);
|
||||
});
|
||||
|
||||
it("does not allow editing by read-only collection user", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
await CollectionUser.update(
|
||||
{
|
||||
createdById: user.id,
|
||||
permission: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -2680,6 +2722,12 @@ describe("#documents.update", () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = CollectionPermission.Read;
|
||||
await collection.save();
|
||||
await CollectionUser.destroy({
|
||||
where: {
|
||||
userId: user.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -2831,10 +2879,6 @@ describe("#documents.update", () => {
|
||||
expect(body.data.document.collectionId).toBe(collection.id);
|
||||
expect(body.data.document.title).toBe("Updated title");
|
||||
expect(body.data.document.text).toBe("Updated text");
|
||||
expect(body.data.collection.icon).toBe(collection.icon);
|
||||
expect(body.data.collection.documents.length).toBe(
|
||||
collection.documentStructure!.length + 1
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ router.post(
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "read", collection);
|
||||
authorize(user, "readDocument", collection);
|
||||
|
||||
// index sort is special because it uses the order of the documents in the
|
||||
// collection.documentStructure rather than a database column
|
||||
@@ -342,7 +342,7 @@ router.post(
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "read", collection);
|
||||
authorize(user, "readDocument", collection);
|
||||
}
|
||||
|
||||
const collectionIds = collectionId
|
||||
@@ -599,7 +599,7 @@ router.post(
|
||||
}
|
||||
|
||||
if (document.collection) {
|
||||
authorize(user, "update", collection);
|
||||
authorize(user, "updateDocument", collection);
|
||||
}
|
||||
|
||||
if (document.deletedAt) {
|
||||
@@ -686,7 +686,7 @@ router.post(
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "read", collection);
|
||||
authorize(user, "readDocument", collection);
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
@@ -780,7 +780,7 @@ router.post(
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "read", collection);
|
||||
authorize(user, "readDocument", collection);
|
||||
}
|
||||
|
||||
let collaboratorIds = undefined;
|
||||
@@ -921,7 +921,7 @@ router.post(
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId!);
|
||||
}
|
||||
authorize(user, "publish", collection);
|
||||
authorize(user, "createDocument", collection);
|
||||
}
|
||||
|
||||
collection = await sequelize.transaction(async (transaction) => {
|
||||
@@ -982,7 +982,7 @@ router.post(
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "update", collection);
|
||||
authorize(user, "updateDocument", collection);
|
||||
|
||||
if (parentDocumentId) {
|
||||
const parent = await Document.findByPk(parentDocumentId, {
|
||||
@@ -1205,7 +1205,7 @@ router.post(
|
||||
teamId: user.teamId,
|
||||
},
|
||||
});
|
||||
authorize(user, "publish", collection);
|
||||
authorize(user, "createDocument", collection);
|
||||
let parentDocument;
|
||||
|
||||
if (parentDocumentId) {
|
||||
@@ -1282,7 +1282,7 @@ router.post(
|
||||
teamId: user.teamId,
|
||||
},
|
||||
});
|
||||
authorize(user, "publish", collection);
|
||||
authorize(user, "createDocument", collection);
|
||||
}
|
||||
|
||||
let parentDocument;
|
||||
|
||||
@@ -8,7 +8,9 @@ const DocumentsSortParamsSchema = z.object({
|
||||
/** Specifies the attributes by which documents will be sorted in the list */
|
||||
sort: z
|
||||
.string()
|
||||
.refine((val) => ["createdAt", "updatedAt", "index", "title"].includes(val))
|
||||
.refine((val) =>
|
||||
["createdAt", "updatedAt", "publishedAt", "index", "title"].includes(val)
|
||||
)
|
||||
.default("updatedAt"),
|
||||
|
||||
/** Specifies the sort order with respect to sort field */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Revision } from "@server/models";
|
||||
import { CollectionUser, Revision } from "@server/models";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { seed, getTestServer } from "@server/test/support";
|
||||
|
||||
@@ -141,6 +141,12 @@ describe("#revisions.list", () => {
|
||||
await Revision.createFromDocument(document);
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.destroy({
|
||||
where: {
|
||||
userId: user.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/revisions.list", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
|
||||
@@ -156,12 +156,18 @@ describe("#shares.create", () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
await CollectionUser.update(
|
||||
{
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
|
||||
Reference in New Issue
Block a user