feat: Allow viewers to be upgraded to editors on individual collections (#4023)
* Improve types * More types, fix default permission for viewers added to collection * fix change of default role for CollectionGroup * Restore policy * test * tests
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { $Diff } from "utility-types";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import InputSelect, { Props, Option } from "./InputSelect";
|
||||
|
||||
export default function InputSelectPermission(
|
||||
@@ -31,11 +32,11 @@ export default function InputSelectPermission(
|
||||
options={[
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: "read_write",
|
||||
value: CollectionPermission.ReadWrite,
|
||||
},
|
||||
{
|
||||
label: t("View only"),
|
||||
value: "read",
|
||||
value: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
label: t("No access"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { trim } from "lodash";
|
||||
import { action, computed, observable } from "mobx";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||
import CollectionsStore from "~/stores/CollectionsStore";
|
||||
import Document from "~/models/Document";
|
||||
@@ -39,7 +40,7 @@ export default class Collection extends ParanoidModel {
|
||||
|
||||
@Field
|
||||
@observable
|
||||
permission: "read" | "read_write" | void;
|
||||
permission: CollectionPermission | void;
|
||||
|
||||
@Field
|
||||
@observable
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { computed } from "mobx";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import BaseModel from "./BaseModel";
|
||||
|
||||
class CollectionGroupMembership extends BaseModel {
|
||||
@@ -8,16 +9,11 @@ class CollectionGroupMembership extends BaseModel {
|
||||
|
||||
collectionId: string;
|
||||
|
||||
permission: string;
|
||||
permission: CollectionPermission;
|
||||
|
||||
@computed
|
||||
get isEditor(): boolean {
|
||||
return this.permission === "read_write";
|
||||
}
|
||||
|
||||
@computed
|
||||
get isMaintainer(): boolean {
|
||||
return this.permission === "maintainer";
|
||||
return this.permission === CollectionPermission.ReadWrite;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { computed } from "mobx";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import BaseModel from "./BaseModel";
|
||||
|
||||
class Membership extends BaseModel {
|
||||
@@ -8,16 +9,11 @@ class Membership extends BaseModel {
|
||||
|
||||
collectionId: string;
|
||||
|
||||
permission: string;
|
||||
permission: CollectionPermission;
|
||||
|
||||
@computed
|
||||
get isEditor(): boolean {
|
||||
return this.permission === "read_write";
|
||||
}
|
||||
|
||||
@computed
|
||||
get isMaintainer(): boolean {
|
||||
return this.permission === "maintainer";
|
||||
return this.permission === CollectionPermission.ReadWrite;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, Trans, WithTranslation } from "react-i18next";
|
||||
import { randomElement } from "@shared/random";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { CollectionValidation } from "@shared/validations";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
@@ -38,7 +39,7 @@ class CollectionNew extends React.Component<Props> {
|
||||
sharing = true;
|
||||
|
||||
@observable
|
||||
permission = "read_write";
|
||||
permission = CollectionPermission.ReadWrite;
|
||||
|
||||
@observable
|
||||
isSaving: boolean;
|
||||
@@ -100,8 +101,8 @@ class CollectionNew extends React.Component<Props> {
|
||||
this.hasOpenedIconPicker = true;
|
||||
};
|
||||
|
||||
handlePermissionChange = (newPermission: string) => {
|
||||
this.permission = newPermission;
|
||||
handlePermissionChange = (permission: CollectionPermission) => {
|
||||
this.permission = permission;
|
||||
};
|
||||
|
||||
handleSharingChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -59,7 +59,6 @@ class AddGroupsToCollection extends React.Component<Props> {
|
||||
this.props.collectionGroupMemberships.create({
|
||||
collectionId: this.props.collection.id,
|
||||
groupId: group.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
this.props.toasts.showToast(
|
||||
t("{{ groupName }} was added to the collection", {
|
||||
|
||||
@@ -57,7 +57,6 @@ class AddPeopleToCollection extends React.Component<Props> {
|
||||
this.props.memberships.create({
|
||||
collectionId: this.props.collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
this.props.toasts.showToast(
|
||||
t("{{ userName }} was added to the collection", {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import CollectionGroupMembership from "~/models/CollectionGroupMembership";
|
||||
import Group from "~/models/Group";
|
||||
import GroupListItem from "~/components/GroupListItem";
|
||||
@@ -10,7 +11,7 @@ import CollectionGroupMemberMenu from "~/menus/CollectionGroupMemberMenu";
|
||||
type Props = {
|
||||
group: Group;
|
||||
collectionGroupMembership: CollectionGroupMembership | null | undefined;
|
||||
onUpdate: (permission: string) => void;
|
||||
onUpdate: (permission: CollectionPermission) => void;
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
@@ -21,19 +22,6 @@ const CollectionGroupMemberListItem = ({
|
||||
onRemove,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const PERMISSIONS = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t("View only"),
|
||||
value: "read",
|
||||
},
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: "read_write",
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<GroupListItem
|
||||
@@ -43,7 +31,16 @@ const CollectionGroupMemberListItem = ({
|
||||
<>
|
||||
<Select
|
||||
label={t("Permissions")}
|
||||
options={PERMISSIONS}
|
||||
options={[
|
||||
{
|
||||
label: t("View only"),
|
||||
value: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: CollectionPermission.ReadWrite,
|
||||
},
|
||||
]}
|
||||
value={
|
||||
collectionGroupMembership
|
||||
? collectionGroupMembership.permission
|
||||
|
||||
@@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Membership from "~/models/Membership";
|
||||
import User from "~/models/User";
|
||||
import Avatar from "~/components/Avatar";
|
||||
@@ -19,7 +20,7 @@ type Props = {
|
||||
canEdit: boolean;
|
||||
onAdd?: () => void;
|
||||
onRemove?: () => void;
|
||||
onUpdate?: (permission: string) => void;
|
||||
onUpdate?: (permission: CollectionPermission) => void;
|
||||
};
|
||||
|
||||
const MemberListItem = ({
|
||||
@@ -31,19 +32,6 @@ const MemberListItem = ({
|
||||
canEdit,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const PERMISSIONS = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t("View only"),
|
||||
value: "read",
|
||||
},
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: "read_write",
|
||||
},
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -67,7 +55,16 @@ const MemberListItem = ({
|
||||
{onUpdate && (
|
||||
<Select
|
||||
label={t("Permissions")}
|
||||
options={PERMISSIONS}
|
||||
options={[
|
||||
{
|
||||
label: t("View only"),
|
||||
value: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: CollectionPermission.ReadWrite,
|
||||
},
|
||||
]}
|
||||
value={membership ? membership.permission : undefined}
|
||||
onChange={onUpdate}
|
||||
disabled={!canEdit}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Collection from "~/models/Collection";
|
||||
import Group from "~/models/Group";
|
||||
import User from "~/models/User";
|
||||
@@ -151,7 +152,7 @@ function CollectionPermissions({ collection }: Props) {
|
||||
);
|
||||
|
||||
const handleChangePermission = React.useCallback(
|
||||
async (permission: string) => {
|
||||
async (permission: CollectionPermission) => {
|
||||
try {
|
||||
await collection.save({
|
||||
permission,
|
||||
@@ -218,9 +219,10 @@ function CollectionPermissions({ collection }: Props) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{collection.permission === "read" && (
|
||||
{collection.permission === CollectionPermission.ReadWrite && (
|
||||
<Trans
|
||||
defaults="Team members can view documents in the <em>{{ collectionName }}</em> collection by default."
|
||||
defaults="Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by
|
||||
default."
|
||||
values={{
|
||||
collectionName,
|
||||
}}
|
||||
@@ -229,10 +231,9 @@ function CollectionPermissions({ collection }: Props) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{collection.permission === "read_write" && (
|
||||
{collection.permission === CollectionPermission.Read && (
|
||||
<Trans
|
||||
defaults="Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by
|
||||
default."
|
||||
defaults="Team members can view documents in the <em>{{ collectionName }}</em> collection by default."
|
||||
values={{
|
||||
collectionName,
|
||||
}}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
||||
import { useState } from "react";
|
||||
import * as React from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Collection from "~/models/Collection";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
@@ -38,8 +39,8 @@ function DocumentReparent({ collection, item, onSubmit, onCancel }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const prevCollection = collections.get(item.collectionId);
|
||||
const accessMapping = {
|
||||
read_write: t("view and edit access"),
|
||||
read: t("view only access"),
|
||||
[CollectionPermission.ReadWrite]: t("view and edit access"),
|
||||
[CollectionPermission.Read]: t("view only access"),
|
||||
null: t("no access"),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import invariant from "invariant";
|
||||
import { action, runInAction } from "mobx";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import CollectionGroupMembership from "~/models/CollectionGroupMembership";
|
||||
import { PaginationParams } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
@@ -45,7 +46,7 @@ export default class CollectionGroupMembershipsStore extends BaseStore<
|
||||
}: {
|
||||
collectionId: string;
|
||||
groupId: string;
|
||||
permission: string;
|
||||
permission?: CollectionPermission;
|
||||
}) {
|
||||
const res = await client.post("/collections.add_group", {
|
||||
id: collectionId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import invariant from "invariant";
|
||||
import { concat, find, last } from "lodash";
|
||||
import { computed, action } from "mobx";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Collection from "~/models/Collection";
|
||||
import { NavigationNode } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
@@ -171,8 +172,10 @@ export default class CollectionsStore extends BaseStore<Collection> {
|
||||
|
||||
@computed
|
||||
get publicCollections() {
|
||||
return this.orderedData.filter((collection) =>
|
||||
["read", "read_write"].includes(collection.permission || "")
|
||||
return this.orderedData.filter(
|
||||
(collection) =>
|
||||
collection.permission &&
|
||||
Object.values(CollectionPermission).includes(collection.permission)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import invariant from "invariant";
|
||||
import { action, runInAction } from "mobx";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Membership from "~/models/Membership";
|
||||
import { PaginationParams } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
@@ -43,7 +44,7 @@ export default class MembershipsStore extends BaseStore<Membership> {
|
||||
}: {
|
||||
collectionId: string;
|
||||
userId: string;
|
||||
permission: string;
|
||||
permission?: CollectionPermission;
|
||||
}) {
|
||||
const res = await client.post("/collections.add_user", {
|
||||
id: collectionId,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
AuthorizationError,
|
||||
UserSuspendedError,
|
||||
} from "../errors";
|
||||
import { ContextWithState, AuthenticationTypes } from "../types";
|
||||
import { ContextWithState, AuthenticationType } from "../types";
|
||||
|
||||
type AuthenticationOptions = {
|
||||
/* An admin user role is required to access the route */
|
||||
@@ -63,7 +63,7 @@ export default function auth(options: AuthenticationOptions = {}) {
|
||||
|
||||
if (token) {
|
||||
if (String(token).match(/^[\w]{38}$/)) {
|
||||
ctx.state.authType = AuthenticationTypes.API;
|
||||
ctx.state.authType = AuthenticationType.API;
|
||||
let apiKey;
|
||||
|
||||
try {
|
||||
@@ -94,7 +94,7 @@ export default function auth(options: AuthenticationOptions = {}) {
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
} else {
|
||||
ctx.state.authType = AuthenticationTypes.APP;
|
||||
ctx.state.authType = AuthenticationType.APP;
|
||||
user = await getUserForJWT(String(token));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,12 @@ import {
|
||||
Length as SimpleLength,
|
||||
} from "sequelize-typescript";
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
||||
import { CollectionValidation } from "@shared/validations";
|
||||
import slugify from "@server/utils/slugify";
|
||||
import { NavigationNode, CollectionSort } from "~/types";
|
||||
import type { NavigationNode, CollectionSort } from "~/types";
|
||||
import CollectionGroup from "./CollectionGroup";
|
||||
import CollectionUser from "./CollectionUser";
|
||||
import Document from "./Document";
|
||||
@@ -39,9 +40,6 @@ import IsHexColor from "./validators/IsHexColor";
|
||||
import Length from "./validators/Length";
|
||||
import NotContainsUrl from "./validators/NotContainsUrl";
|
||||
|
||||
// without this indirection, the app crashes on starup
|
||||
type Sort = CollectionSort;
|
||||
|
||||
@Scopes(() => ({
|
||||
withAllMemberships: {
|
||||
include: [
|
||||
@@ -174,9 +172,9 @@ class Collection extends ParanoidModel {
|
||||
@Column
|
||||
index: string | null;
|
||||
|
||||
@IsIn([["read", "read_write"]])
|
||||
@Column
|
||||
permission: "read" | "read_write" | null;
|
||||
@IsIn([Object.values(CollectionPermission)])
|
||||
@Column(DataType.STRING)
|
||||
permission: CollectionPermission | null;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
@@ -193,7 +191,7 @@ class Collection extends ParanoidModel {
|
||||
@Column({
|
||||
type: DataType.JSONB,
|
||||
validate: {
|
||||
isSort(value: Sort) {
|
||||
isSort(value: CollectionSort) {
|
||||
if (
|
||||
typeof value !== "object" ||
|
||||
!value.direction ||
|
||||
@@ -213,7 +211,7 @@ class Collection extends ParanoidModel {
|
||||
},
|
||||
},
|
||||
})
|
||||
sort: Sort;
|
||||
sort: CollectionSort;
|
||||
|
||||
// getters
|
||||
|
||||
@@ -255,14 +253,14 @@ class Collection extends ParanoidModel {
|
||||
model: Collection,
|
||||
options: { transaction: Transaction }
|
||||
) {
|
||||
if (model.permission !== "read_write") {
|
||||
if (model.permission !== CollectionPermission.ReadWrite) {
|
||||
return CollectionUser.findOrCreate({
|
||||
where: {
|
||||
collectionId: model.id,
|
||||
userId: model.createdById,
|
||||
},
|
||||
defaults: {
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
createdById: model.createdById,
|
||||
},
|
||||
transaction: options.transaction,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DataType,
|
||||
Scopes,
|
||||
} from "sequelize-typescript";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Collection from "./Collection";
|
||||
import Group from "./Group";
|
||||
import User from "./User";
|
||||
@@ -33,10 +34,10 @@ import Fix from "./decorators/Fix";
|
||||
@Table({ tableName: "collection_groups", modelName: "collection_group" })
|
||||
@Fix
|
||||
class CollectionGroup extends BaseModel {
|
||||
@Default("read_write")
|
||||
@IsIn([["read", "read_write", "maintainer"]])
|
||||
@Column
|
||||
permission: string;
|
||||
@Default(CollectionPermission.ReadWrite)
|
||||
@IsIn([Object.values(CollectionPermission)])
|
||||
@Column(DataType.STRING)
|
||||
permission: CollectionPermission;
|
||||
|
||||
// associations
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DataType,
|
||||
Scopes,
|
||||
} from "sequelize-typescript";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Collection from "./Collection";
|
||||
import User from "./User";
|
||||
import BaseModel from "./base/BaseModel";
|
||||
@@ -32,10 +33,10 @@ import Fix from "./decorators/Fix";
|
||||
@Table({ tableName: "collection_users", modelName: "collection_user" })
|
||||
@Fix
|
||||
class CollectionUser extends BaseModel {
|
||||
@Default("read_write")
|
||||
@IsIn([["read", "read_write", "maintainer"]])
|
||||
@Column
|
||||
permission: string;
|
||||
@Default(CollectionPermission.ReadWrite)
|
||||
@IsIn([Object.values(CollectionPermission)])
|
||||
@Column(DataType.STRING)
|
||||
permission: CollectionPermission;
|
||||
|
||||
// associations
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
IsUrl,
|
||||
AllowNull,
|
||||
} from "sequelize-typescript";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
|
||||
import env from "@server/env";
|
||||
import { generateAvatarUrl } from "@server/utils/avatars";
|
||||
@@ -172,7 +173,7 @@ class Team extends ParanoidModel {
|
||||
teamId: this.id,
|
||||
createdById: userId,
|
||||
sort: Collection.DEFAULT_SORT,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { buildUser, buildTeam, buildCollection } from "@server/test/factories";
|
||||
import { getTestDatabase } from "@server/test/support";
|
||||
import CollectionUser from "./CollectionUser";
|
||||
@@ -39,7 +40,7 @@ describe("user model", () => {
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const response = await user.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
@@ -52,7 +53,7 @@ describe("user model", () => {
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const response = await user.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
@@ -83,7 +84,7 @@ describe("user model", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const response = await user.collectionIds();
|
||||
expect(response.length).toEqual(1);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
AllowNull,
|
||||
} from "sequelize-typescript";
|
||||
import { languages } from "@shared/i18n";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { stringToColor } from "@shared/utils/color";
|
||||
import env from "@server/env";
|
||||
import { ValidationError } from "../errors";
|
||||
@@ -219,6 +220,12 @@ class User extends ParanoidModel {
|
||||
return stringToColor(this.id);
|
||||
}
|
||||
|
||||
get defaultCollectionPermission(): CollectionPermission {
|
||||
return this.isViewer
|
||||
? CollectionPermission.Read
|
||||
: CollectionPermission.ReadWrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a code that can be used to delete this user account. The code will
|
||||
* be rotated when the user signs out.
|
||||
@@ -298,8 +305,8 @@ class User extends ParanoidModel {
|
||||
return collectionStubs
|
||||
.filter(
|
||||
(c) =>
|
||||
c.permission === "read" ||
|
||||
c.permission === "read_write" ||
|
||||
c.permission === CollectionPermission.Read ||
|
||||
c.permission === CollectionPermission.ReadWrite ||
|
||||
c.memberships.length > 0 ||
|
||||
c.collectionGroupMemberships.length > 0
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { CollectionUser, Collection } from "@server/models";
|
||||
import { buildUser, buildTeam, buildCollection } from "@server/test/factories";
|
||||
import { getTestDatabase } from "@server/test/support";
|
||||
@@ -9,6 +10,7 @@ afterAll(db.disconnect);
|
||||
|
||||
beforeEach(db.flush);
|
||||
|
||||
describe("member", () => {
|
||||
describe("read_write permission", () => {
|
||||
it("should allow read write permissions for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
@@ -17,7 +19,7 @@ describe("read_write permission", () => {
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
@@ -32,13 +34,13 @@ describe("read_write permission", () => {
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
// reload to get membership
|
||||
const reloaded = await Collection.scope({
|
||||
@@ -59,7 +61,7 @@ describe("read permission", () => {
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
@@ -74,13 +76,13 @@ describe("read permission", () => {
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
// reload to get membership
|
||||
const reloaded = await Collection.scope({
|
||||
@@ -122,7 +124,7 @@ describe("no permission", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
// reload to get membership
|
||||
const reloaded = await Collection.scope({
|
||||
@@ -134,3 +136,122 @@ describe("no permission", () => {
|
||||
expect(abilities.share).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("viewer", () => {
|
||||
describe("read_write permission", () => {
|
||||
it("should allow read permissions for viewer", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
isViewer: true,
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
});
|
||||
|
||||
it("should override read membership permission", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
isViewer: true,
|
||||
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.ReadWrite,
|
||||
});
|
||||
// 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.update).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("read permission", () => {
|
||||
it("should allow override with read_write membership permission", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
isViewer: true,
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
// 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.update).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no permission", () => {
|
||||
it("should allow no permissions for viewer", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
isViewer: true,
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
const abilities = serialize(user, collection);
|
||||
expect(abilities.read).toEqual(false);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
});
|
||||
|
||||
it("should allow override with team member membership permission", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
isViewer: true,
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: null,
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
// 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.update).toEqual(true);
|
||||
expect(abilities.share).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import invariant from "invariant";
|
||||
import { some } from "lodash";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { Collection, User, Team } from "@server/models";
|
||||
import { AdminRequiredError } from "../errors";
|
||||
import { allow } from "./cancan";
|
||||
@@ -57,7 +58,7 @@ allow(User, ["read", "star", "unstar"], Collection, (user, collection) => {
|
||||
...collection.collectionGroupMemberships,
|
||||
];
|
||||
return some(allMemberships, (m) =>
|
||||
["read", "read_write", "maintainer"].includes(m.permission)
|
||||
Object.values(CollectionPermission).includes(m.permission)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,9 +66,6 @@ allow(User, ["read", "star", "unstar"], Collection, (user, collection) => {
|
||||
});
|
||||
|
||||
allow(User, "share", Collection, (user, collection) => {
|
||||
if (user.isViewer) {
|
||||
return false;
|
||||
}
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
@@ -78,7 +76,10 @@ allow(User, "share", Collection, (user, collection) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (collection.permission !== "read_write") {
|
||||
if (
|
||||
collection.permission !== CollectionPermission.ReadWrite ||
|
||||
user.isViewer
|
||||
) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
@@ -87,8 +88,9 @@ allow(User, "share", Collection, (user, collection) => {
|
||||
...collection.memberships,
|
||||
...collection.collectionGroupMemberships,
|
||||
];
|
||||
return some(allMemberships, (m) =>
|
||||
["read_write", "maintainer"].includes(m.permission)
|
||||
return some(
|
||||
allMemberships,
|
||||
(m) => m.permission === CollectionPermission.ReadWrite
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,9 +98,6 @@ allow(User, "share", Collection, (user, collection) => {
|
||||
});
|
||||
|
||||
allow(User, ["publish", "update"], Collection, (user, collection) => {
|
||||
if (user.isViewer) {
|
||||
return false;
|
||||
}
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
@@ -106,7 +105,10 @@ allow(User, ["publish", "update"], Collection, (user, collection) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (collection.permission !== "read_write") {
|
||||
if (
|
||||
collection.permission !== CollectionPermission.ReadWrite ||
|
||||
user.isViewer
|
||||
) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
@@ -115,8 +117,9 @@ allow(User, ["publish", "update"], Collection, (user, collection) => {
|
||||
...collection.memberships,
|
||||
...collection.collectionGroupMemberships,
|
||||
];
|
||||
return some(allMemberships, (m) =>
|
||||
["read_write", "maintainer"].includes(m.permission)
|
||||
return some(
|
||||
allMemberships,
|
||||
(m) => m.permission === CollectionPermission.ReadWrite
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,9 +127,6 @@ allow(User, ["publish", "update"], Collection, (user, collection) => {
|
||||
});
|
||||
|
||||
allow(User, "delete", Collection, (user, collection) => {
|
||||
if (user.isViewer) {
|
||||
return false;
|
||||
}
|
||||
if (!collection || user.teamId !== collection.teamId) {
|
||||
return false;
|
||||
}
|
||||
@@ -134,7 +134,10 @@ allow(User, "delete", Collection, (user, collection) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (collection.permission !== "read_write") {
|
||||
if (
|
||||
collection.permission !== CollectionPermission.ReadWrite ||
|
||||
user.isViewer
|
||||
) {
|
||||
invariant(
|
||||
collection.memberships,
|
||||
"membership should be preloaded, did you forget withMembership scope?"
|
||||
@@ -143,8 +146,9 @@ allow(User, "delete", Collection, (user, collection) => {
|
||||
...collection.memberships,
|
||||
...collection.collectionGroupMemberships,
|
||||
];
|
||||
return some(allMemberships, (m) =>
|
||||
["read_write", "maintainer"].includes(m.permission)
|
||||
return some(
|
||||
allMemberships,
|
||||
(m) => m.permission === CollectionPermission.ReadWrite
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import {
|
||||
buildUser,
|
||||
buildTeam,
|
||||
@@ -14,14 +15,14 @@ afterAll(db.disconnect);
|
||||
beforeEach(db.flush);
|
||||
|
||||
describe("read_write collection", () => {
|
||||
it("should allow read write permissions for team member", async () => {
|
||||
it("should allow read write permissions for member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
@@ -37,17 +38,42 @@ describe("read_write collection", () => {
|
||||
expect(abilities.share).toEqual(true);
|
||||
expect(abilities.move).toEqual(true);
|
||||
});
|
||||
|
||||
it("should allow read permissions for viewer", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
isViewer: true,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
const abilities = serialize(user, document);
|
||||
expect(abilities.read).toEqual(true);
|
||||
expect(abilities.download).toEqual(true);
|
||||
expect(abilities.update).toEqual(false);
|
||||
expect(abilities.createChildDocument).toEqual(false);
|
||||
expect(abilities.archive).toEqual(false);
|
||||
expect(abilities.delete).toEqual(false);
|
||||
expect(abilities.share).toEqual(false);
|
||||
expect(abilities.move).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("read collection", () => {
|
||||
it("should allow read only permissions permissions for team member", async () => {
|
||||
it("should allow read permissions for team member", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
teamId: team.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: team.id,
|
||||
|
||||
@@ -209,9 +209,6 @@ allow(User, "delete", Document, (user, document) => {
|
||||
if (document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
if (user.isViewer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// allow deleting document without a collection
|
||||
if (document.collection && cannot(user, "update", document.collection)) {
|
||||
@@ -237,9 +234,6 @@ allow(User, "permanentDelete", Document, (user, document) => {
|
||||
if (!document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
if (user.isViewer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// allow deleting document without a collection
|
||||
if (document.collection && cannot(user, "update", document.collection)) {
|
||||
@@ -256,9 +250,6 @@ allow(User, "restore", Document, (user, document) => {
|
||||
if (!document.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
if (user.isViewer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.collection && cannot(user, "update", document.collection)) {
|
||||
return false;
|
||||
|
||||
@@ -22,6 +22,7 @@ it("should allow reading only", async () => {
|
||||
expect(abilities.createGroup).toEqual(false);
|
||||
expect(abilities.createIntegration).toEqual(false);
|
||||
});
|
||||
|
||||
it("should allow admins to manage", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { CollectionGroup } from "@server/models";
|
||||
|
||||
type Membership = {
|
||||
id: string;
|
||||
groupId: string;
|
||||
collectionId: string;
|
||||
permission: string;
|
||||
permission: CollectionPermission;
|
||||
};
|
||||
|
||||
export default (membership: CollectionGroup): Membership => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { CollectionUser } from "@server/models";
|
||||
|
||||
type Membership = {
|
||||
id: string;
|
||||
userId: string;
|
||||
collectionId: string;
|
||||
permission: string;
|
||||
permission: CollectionPermission;
|
||||
};
|
||||
|
||||
export default (membership: CollectionUser): Membership => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { S3 } from "aws-sdk";
|
||||
import { truncate } from "lodash";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { CollectionValidation } from "@shared/validations";
|
||||
import attachmentCreator from "@server/commands/attachmentCreator";
|
||||
import documentCreator from "@server/commands/documentCreator";
|
||||
@@ -272,7 +273,7 @@ export default abstract class ImportTask extends BaseTask<Props> {
|
||||
length: CollectionValidation.maxDescriptionLength,
|
||||
}),
|
||||
createdById: fileOperation.userId,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
@@ -292,7 +293,7 @@ export default abstract class ImportTask extends BaseTask<Props> {
|
||||
teamId: fileOperation.teamId,
|
||||
createdById: fileOperation.userId,
|
||||
name,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
@@ -130,12 +130,3 @@ Object {
|
||||
"status": 401,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#collections.users should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
"message": "Authentication required",
|
||||
"ok": false,
|
||||
"status": 401,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { Document, CollectionUser, CollectionGroup } from "@server/models";
|
||||
import {
|
||||
@@ -101,7 +102,7 @@ describe("#collections.list", () => {
|
||||
});
|
||||
await collection.$add("group", group, {
|
||||
through: {
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
@@ -293,7 +294,7 @@ describe("#collections.export", () => {
|
||||
createdById: admin.id,
|
||||
collectionId: collection.id,
|
||||
userId: admin.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.export", {
|
||||
body: {
|
||||
@@ -320,7 +321,7 @@ describe("#collections.export", () => {
|
||||
});
|
||||
await collection.$add("group", group, {
|
||||
through: {
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
createdById: admin.id,
|
||||
},
|
||||
});
|
||||
@@ -671,48 +672,6 @@ describe("#collections.remove_user", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#collections.users", () => {
|
||||
it("should return users in private collection", async () => {
|
||||
const { collection, user } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
});
|
||||
const res = await server.post("/api/collections.users", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
});
|
||||
|
||||
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.group_memberships", () => {
|
||||
it("should return groups in private collection", async () => {
|
||||
const user = await buildUser();
|
||||
@@ -727,13 +686,13 @@ describe("#collections.group_memberships", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionGroup.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
groupId: group.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.group_memberships", {
|
||||
body: {
|
||||
@@ -747,7 +706,7 @@ describe("#collections.group_memberships", () => {
|
||||
expect(body.data.groups[0].id).toEqual(group.id);
|
||||
expect(body.data.collectionGroupMemberships.length).toEqual(1);
|
||||
expect(body.data.collectionGroupMemberships[0].permission).toEqual(
|
||||
"read_write"
|
||||
CollectionPermission.ReadWrite
|
||||
);
|
||||
});
|
||||
|
||||
@@ -769,19 +728,19 @@ describe("#collections.group_memberships", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionGroup.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
groupId: group.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionGroup.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
groupId: group2.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.group_memberships", {
|
||||
body: {
|
||||
@@ -812,25 +771,25 @@ describe("#collections.group_memberships", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionGroup.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
groupId: group.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionGroup.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
groupId: group2.id,
|
||||
permission: "maintainer",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const res = await server.post("/api/collections.group_memberships", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
permission: "maintainer",
|
||||
permission: CollectionPermission.Read,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
@@ -871,7 +830,7 @@ describe("#collections.memberships", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.memberships", {
|
||||
body: {
|
||||
@@ -884,7 +843,9 @@ describe("#collections.memberships", () => {
|
||||
expect(body.data.users.length).toEqual(1);
|
||||
expect(body.data.users[0].id).toEqual(user.id);
|
||||
expect(body.data.memberships.length).toEqual(1);
|
||||
expect(body.data.memberships[0].permission).toEqual("read_write");
|
||||
expect(body.data.memberships[0].permission).toEqual(
|
||||
CollectionPermission.ReadWrite
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow filtering members in collection by name", async () => {
|
||||
@@ -896,13 +857,13 @@ describe("#collections.memberships", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user2.id,
|
||||
collectionId: collection.id,
|
||||
userId: user2.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.memberships", {
|
||||
body: {
|
||||
@@ -924,19 +885,19 @@ describe("#collections.memberships", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
await CollectionUser.create({
|
||||
createdById: user2.id,
|
||||
collectionId: collection.id,
|
||||
userId: user2.id,
|
||||
permission: "maintainer",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const res = await server.post("/api/collections.memberships", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
permission: "maintainer",
|
||||
permission: CollectionPermission.Read,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
@@ -1000,7 +961,7 @@ describe("#collections.info", () => {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const res = await server.post("/api/collections.info", {
|
||||
body: {
|
||||
@@ -1282,20 +1243,20 @@ describe("#collections.update", () => {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
name: "Test",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toBe("Test");
|
||||
expect(body.data.permission).toBe("read_write");
|
||||
expect(body.data.permission).toBe(CollectionPermission.ReadWrite);
|
||||
// ensure we return with a write level policy
|
||||
expect(body.policies.length).toBe(1);
|
||||
expect(body.policies[0].abilities.update).toBe(true);
|
||||
@@ -1309,7 +1270,7 @@ describe("#collections.update", () => {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
@@ -1340,7 +1301,7 @@ describe("#collections.update", () => {
|
||||
});
|
||||
await collection.$add("group", group, {
|
||||
through: {
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
@@ -1365,7 +1326,7 @@ describe("#collections.update", () => {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const res = await server.post("/api/collections.update", {
|
||||
body: {
|
||||
@@ -1508,7 +1469,7 @@ describe("#collections.delete", () => {
|
||||
});
|
||||
await collection.$add("group", group, {
|
||||
through: {
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import invariant from "invariant";
|
||||
import Router from "koa-router";
|
||||
import { Sequelize, Op, WhereOptions } from "sequelize";
|
||||
import { randomElement } from "@shared/random";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { RateLimiterStrategy } from "@server/RateLimiter";
|
||||
import collectionExporter from "@server/commands/collectionExporter";
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
assertPresent,
|
||||
assertHexColor,
|
||||
assertIndexCharacters,
|
||||
assertCollectionPermission,
|
||||
} from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
@@ -199,9 +201,10 @@ router.post(
|
||||
);
|
||||
|
||||
router.post("collections.add_group", auth(), async (ctx) => {
|
||||
const { id, groupId, permission = "read_write" } = ctx.body;
|
||||
const { id, groupId, permission = CollectionPermission.ReadWrite } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
assertUuid(groupId, "groupId is required");
|
||||
assertCollectionPermission(permission);
|
||||
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", ctx.state.user.id],
|
||||
@@ -340,7 +343,7 @@ router.post(
|
||||
);
|
||||
|
||||
router.post("collections.add_user", auth(), async (ctx) => {
|
||||
const { id, userId, permission = "read_write" } = ctx.body;
|
||||
const { id, userId, permission } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
assertUuid(userId, "userId is required");
|
||||
|
||||
@@ -359,11 +362,15 @@ router.post("collections.add_user", auth(), async (ctx) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (permission) {
|
||||
assertCollectionPermission(permission);
|
||||
}
|
||||
|
||||
if (!membership) {
|
||||
membership = await CollectionUser.create({
|
||||
collectionId: id,
|
||||
userId,
|
||||
permission,
|
||||
permission: permission || user.defaultCollectionPermission,
|
||||
createdById: ctx.state.user.id,
|
||||
});
|
||||
} else if (permission) {
|
||||
@@ -422,24 +429,6 @@ router.post("collections.remove_user", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
// DEPRECATED: Use collection.memberships which has pagination, filtering and permissions
|
||||
router.post("collections.users", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
authorize(user, "read", collection);
|
||||
|
||||
const users = await collection.$get("users");
|
||||
|
||||
ctx.body = {
|
||||
data: users.map((user) => presentUser(user)),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("collections.memberships", auth(), pagination(), async (ctx) => {
|
||||
const { id, query, permission } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
@@ -464,6 +453,7 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => {
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
assertCollectionPermission(permission);
|
||||
where = { ...where, permission };
|
||||
}
|
||||
|
||||
@@ -575,16 +565,19 @@ router.post("collections.update", auth(), async (ctx) => {
|
||||
}).findByPk(id);
|
||||
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
|
||||
if (permission !== "read_write" && collection.permission === "read_write") {
|
||||
// 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
|
||||
if (
|
||||
permission !== CollectionPermission.ReadWrite &&
|
||||
collection.permission === CollectionPermission.ReadWrite
|
||||
) {
|
||||
await CollectionUser.findOrCreate({
|
||||
where: {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
},
|
||||
defaults: {
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
@@ -610,12 +603,9 @@ router.post("collections.update", auth(), async (ctx) => {
|
||||
}
|
||||
|
||||
if (permission !== undefined) {
|
||||
// frontend sends empty string
|
||||
assertIn(
|
||||
permission,
|
||||
["read_write", "read", "", null],
|
||||
"Invalid permission"
|
||||
);
|
||||
if (permission) {
|
||||
assertCollectionPermission(permission);
|
||||
}
|
||||
privacyChanged = permission !== collection.permission;
|
||||
collection.permission = permission ? permission : null;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import {
|
||||
Document,
|
||||
View,
|
||||
@@ -800,7 +801,7 @@ describe("#documents.list", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const res = await server.post("/api/documents.list", {
|
||||
body: {
|
||||
@@ -1244,7 +1245,7 @@ describe("#documents.search", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
title: "search term",
|
||||
@@ -2005,7 +2006,7 @@ describe("#documents.update", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
@@ -2094,7 +2095,7 @@ describe("#documents.update", () => {
|
||||
collectionId: collection.id,
|
||||
userId: admin.id,
|
||||
createdById: admin.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
@@ -2118,7 +2119,7 @@ describe("#documents.update", () => {
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
@@ -2133,7 +2134,7 @@ describe("#documents.update", () => {
|
||||
|
||||
it("does not allow editing in read-only collection", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = "read";
|
||||
collection.permission = CollectionPermission.Read;
|
||||
await collection.save();
|
||||
const res = await server.post("/api/documents.update", {
|
||||
body: {
|
||||
|
||||
@@ -326,11 +326,7 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post(
|
||||
"documents.drafts",
|
||||
auth({ member: true }),
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
router.post("documents.drafts", auth(), pagination(), async (ctx) => {
|
||||
let { direction } = ctx.body;
|
||||
const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;
|
||||
|
||||
@@ -394,8 +390,7 @@ router.post(
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"documents.info",
|
||||
@@ -777,7 +772,7 @@ router.post("documents.templatize", auth({ member: true }), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.update", auth({ member: true }), async (ctx) => {
|
||||
router.post("documents.update", auth(), async (ctx) => {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
@@ -837,7 +832,7 @@ router.post("documents.update", auth({ member: true }), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.move", auth({ member: true }), async (ctx) => {
|
||||
router.post("documents.move", auth(), async (ctx) => {
|
||||
const { id, collectionId, parentDocumentId, index } = ctx.body;
|
||||
assertUuid(id, "id must be a uuid");
|
||||
assertUuid(collectionId, "collectionId must be a uuid");
|
||||
@@ -903,7 +898,7 @@ router.post("documents.move", auth({ member: true }), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.archive", auth({ member: true }), async (ctx) => {
|
||||
router.post("documents.archive", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -932,7 +927,7 @@ router.post("documents.archive", auth({ member: true }), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.delete", auth({ member: true }), async (ctx) => {
|
||||
router.post("documents.delete", auth(), async (ctx) => {
|
||||
const { id, permanent } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -993,7 +988,7 @@ router.post("documents.delete", auth({ member: true }), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.unpublish", auth({ member: true }), async (ctx) => {
|
||||
router.post("documents.unpublish", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -1027,7 +1022,7 @@ router.post("documents.unpublish", auth({ member: true }), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.import", auth({ member: true }), async (ctx) => {
|
||||
router.post("documents.import", auth(), async (ctx) => {
|
||||
const { publish, collectionId, parentDocumentId, index } = ctx.body;
|
||||
|
||||
if (!ctx.is("multipart/form-data")) {
|
||||
@@ -1052,7 +1047,6 @@ router.post("documents.import", auth({ member: true }), async (ctx) => {
|
||||
assertPositiveInteger(index, "index must be an integer (>=0)");
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "createDocument", user.team);
|
||||
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
@@ -1110,7 +1104,7 @@ router.post("documents.import", auth({ member: true }), async (ctx) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.post("documents.create", auth({ member: true }), async (ctx) => {
|
||||
router.post("documents.create", auth(), async (ctx) => {
|
||||
const {
|
||||
title = "",
|
||||
text = "",
|
||||
@@ -1132,7 +1126,6 @@ router.post("documents.create", auth({ member: true }), async (ctx) => {
|
||||
assertPositiveInteger(index, "index must be an integer (>=0)");
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "createDocument", user.team);
|
||||
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { CollectionUser } from "@server/models";
|
||||
import {
|
||||
buildUser,
|
||||
@@ -164,7 +165,7 @@ describe("#shares.create", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { View, CollectionUser } from "@server/models";
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { seed, getTestDatabase, getTestServer } from "@server/test/support";
|
||||
@@ -56,7 +57,7 @@ describe("#views.list", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
await View.incrementOrCreate({
|
||||
documentId: document.id,
|
||||
@@ -121,7 +122,7 @@ describe("#views.create", () => {
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
permission: CollectionPermission.Read,
|
||||
});
|
||||
const res = await server.post("/api/views.create", {
|
||||
body: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import {
|
||||
Share,
|
||||
Team,
|
||||
@@ -271,7 +272,7 @@ export async function buildCollection(
|
||||
name: `Test Collection ${count}`,
|
||||
description: "Test collection description",
|
||||
createdById: overrides.userId,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import TestServer from "fetch-test-server";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { sequelize as db } from "@server/database/sequelize";
|
||||
import { User, Document, Collection, Team } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
@@ -68,7 +69,7 @@ export const seed = async () => {
|
||||
urlId: "collection",
|
||||
teamId: team.id,
|
||||
createdById: user.id,
|
||||
permission: "read_write",
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Context } from "koa";
|
||||
import { FileOperation, Team, User } from "./models";
|
||||
|
||||
export enum AuthenticationTypes {
|
||||
export enum AuthenticationType {
|
||||
API = "api",
|
||||
APP = "app",
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export type ContextWithState = Context & {
|
||||
state: {
|
||||
user: User;
|
||||
token: string;
|
||||
authType: AuthenticationTypes;
|
||||
authType: AuthenticationType;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isArrayLike } from "lodash";
|
||||
import validator from "validator";
|
||||
import { CollectionPermission } from "../shared/types";
|
||||
import { validateColorHex } from "../shared/utils/color";
|
||||
import { validateIndexCharacters } from "../shared/utils/indexCharacters";
|
||||
import { ParamRequiredError, ValidationError } from "./errors";
|
||||
@@ -104,3 +105,10 @@ export const assertIndexCharacters = (
|
||||
throw ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
export const assertCollectionPermission = (
|
||||
value: string,
|
||||
message = "Invalid permission"
|
||||
) => {
|
||||
assertIn(value, [...Object.values(CollectionPermission), null], message);
|
||||
};
|
||||
|
||||
@@ -389,8 +389,8 @@
|
||||
"Public document sharing permissions were updated": "Public document sharing permissions were updated",
|
||||
"Could not update public document sharing": "Could not update public document sharing",
|
||||
"The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.": "The <em>{{ collectionName }}</em> collection is private. Team members have no access to it by default.",
|
||||
"Team members can view documents in the <em>{{ collectionName }}</em> collection by default.": "Team members can view documents in the <em>{{ collectionName }}</em> collection by default.",
|
||||
"Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.": "Team members can view and edit documents in the <em>{{ collectionName }}</em> collection by\n default.",
|
||||
"Team members can view documents in the <em>{{ collectionName }}</em> collection by default.": "Team members can view documents in the <em>{{ collectionName }}</em> collection by default.",
|
||||
"When enabled, documents can be shared publicly on the internet.": "When enabled, documents can be shared publicly on the internet.",
|
||||
"Public sharing is currently disabled in the team security settings.": "Public sharing is currently disabled in the team security settings.",
|
||||
"Additional access": "Additional access",
|
||||
|
||||
@@ -28,6 +28,11 @@ export enum IntegrationType {
|
||||
Embed = "embed",
|
||||
}
|
||||
|
||||
export enum CollectionPermission {
|
||||
Read = "read",
|
||||
ReadWrite = "read_write",
|
||||
}
|
||||
|
||||
export type IntegrationSettings<T> = T extends IntegrationType.Embed
|
||||
? { url: string }
|
||||
: T extends IntegrationType.Post
|
||||
|
||||
Reference in New Issue
Block a user