chore: Typescript database models (#2886)

closes #2798
This commit is contained in:
Tom Moor
2022-01-06 18:24:28 -08:00
committed by GitHub
parent d3cbf250e6
commit b20a341f0c
207 changed files with 5624 additions and 5315 deletions

View File

@@ -1,23 +1,24 @@
import Router from "koa-router";
import auth from "@server/middlewares/authentication";
import { ApiKey, Event } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentApiKey } from "@server/presenters";
import { assertUuid, assertPresent } from "@server/validation";
import pagination from "./middlewares/pagination";
const { authorize } = policy;
const router = new Router();
router.post("apiKeys.create", auth(), async (ctx) => {
const { name } = ctx.body;
assertPresent(name, "name is required");
const user = ctx.state.user;
const { user } = ctx.state;
authorize(user, "createApiKey", user.team);
const key = await ApiKey.create({
name,
userId: user.id,
});
await Event.create({
name: "api_keys.create",
modelId: key.id,
@@ -28,13 +29,14 @@ router.post("apiKeys.create", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
data: presentApiKey(key),
};
});
router.post("apiKeys.list", auth(), pagination(), async (ctx) => {
const user = ctx.state.user;
const { user } = ctx.state;
const keys = await ApiKey.findAll({
where: {
userId: user.id,
@@ -43,6 +45,7 @@ router.post("apiKeys.list", auth(), pagination(), async (ctx) => {
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
ctx.body = {
pagination: ctx.state.pagination,
data: keys.map(presentApiKey),
@@ -52,9 +55,10 @@ router.post("apiKeys.list", auth(), pagination(), async (ctx) => {
router.post("apiKeys.delete", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const key = await ApiKey.findByPk(id);
authorize(user, "delete", key);
await key.destroy();
await Event.create({
name: "api_keys.delete",
@@ -66,6 +70,7 @@ router.post("apiKeys.delete", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};

View File

@@ -1,6 +1,6 @@
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'fetc... Remove this comment to see the full error message
import TestServer from "fetch-test-server";
import { Attachment } from "@server/models";
import Attachment from "@server/models/Attachment";
import webService from "@server/services/web";
import {
buildUser,
@@ -13,19 +13,10 @@ import { flushdb } from "@server/test/support";
const app = webService();
const server = new TestServer(app.callback());
jest.mock("aws-sdk", () => {
const mS3 = {
createPresignedPost: jest.fn(),
deleteObject: jest.fn().mockReturnThis(),
promise: jest.fn(),
};
return {
S3: jest.fn(() => mS3),
Endpoint: jest.fn(),
};
});
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#attachments.delete", () => {
it("should require authentication", async () => {
const res = await server.post("/api/attachments.delete");
@@ -120,12 +111,12 @@ describe("#attachments.delete", () => {
});
const document = await buildDocument({
teamId: collection.teamId,
userId: collection.userId,
userId: collection.createdById,
collectionId: collection.id,
});
const attachment = await buildAttachment({
teamId: document.teamId,
userId: document.userId,
userId: document.createdById,
documentId: document.id,
acl: "private",
});
@@ -138,6 +129,7 @@ describe("#attachments.delete", () => {
expect(res.status).toEqual(403);
});
});
describe("#attachments.redirect", () => {
it("should require authentication", async () => {
const res = await server.post("/api/attachments.redirect");
@@ -221,12 +213,12 @@ describe("#attachments.redirect", () => {
});
const document = await buildDocument({
teamId: collection.teamId,
userId: collection.userId,
userId: collection.createdById,
collectionId: collection.id,
});
const attachment = await buildAttachment({
teamId: document.teamId,
userId: document.userId,
userId: document.createdById,
documentId: document.id,
acl: "private",
});

View File

@@ -3,7 +3,7 @@ import { v4 as uuidv4 } from "uuid";
import { NotFoundError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { Attachment, Document, Event } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import {
getPresignedPost,
publicS3Endpoint,
@@ -11,7 +11,6 @@ import {
} from "@server/utils/s3";
import { assertPresent } from "@server/validation";
const { authorize } = policy;
const router = new Router();
const AWS_S3_ACL = process.env.AWS_S3_ACL || "private";
@@ -61,6 +60,7 @@ router.post("attachments.create", auth(), async (ctx) => {
userId: user.id,
ip: ctx.request.ip,
});
ctx.body = {
data: {
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
@@ -85,7 +85,7 @@ router.post("attachments.create", auth(), async (ctx) => {
router.post("attachments.delete", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const attachment = await Attachment.findByPk(id);
if (!attachment) {
@@ -107,6 +107,7 @@ router.post("attachments.delete", auth(), async (ctx) => {
userId: user.id,
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
@@ -115,7 +116,7 @@ router.post("attachments.delete", auth(), async (ctx) => {
router.post("attachments.redirect", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const attachment = await Attachment.findByPk(id);
if (!attachment) {

View File

@@ -8,6 +8,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#auth.info", () => {
it("should return current authentication", async () => {
const team = await buildTeam();
@@ -44,6 +45,7 @@ describe("#auth.info", () => {
expect(res.status).toEqual(401);
});
});
describe("#auth.config", () => {
it("should return available SSO providers", async () => {
const res = await server.post("/api/auth.config");

View File

@@ -1,3 +1,4 @@
import invariant from "invariant";
import Router from "koa-router";
import { find } from "lodash";
import { parseDomain, isCustomSubdomain } from "@shared/utils/domains";
@@ -5,14 +6,11 @@ import auth from "@server/middlewares/authentication";
import { Team } from "@server/models";
import { presentUser, presentTeam, presentPolicies } from "@server/presenters";
import { isCustomDomain } from "@server/utils/domains";
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'providers' implicitly has type 'any[]' i... Remove this comment to see the full error message
import providers from "../auth/providers";
const router = new Router();
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'team' implicitly has an 'any' type.
function filterProviders(team) {
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message
function filterProviders(team: Team) {
return providers
.sort((provider) => (provider.id === "email" ? 1 : -1))
.filter((provider) => {
@@ -112,8 +110,10 @@ router.post("auth.config", async (ctx) => {
});
router.post("auth.info", auth(), async (ctx) => {
const user = ctx.state.user;
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
invariant(team, "Team not found");
ctx.body = {
data: {
user: presentUser(user, {

View File

@@ -17,7 +17,7 @@ describe("#authenticationProviders.info", () => {
const user = await buildUser({
teamId: team.id,
});
const authenticationProviders = await team.getAuthenticationProviders();
const authenticationProviders = await team.$get("authenticationProviders");
const res = await server.post("/api/authenticationProviders.info", {
body: {
id: authenticationProviders[0].id,
@@ -36,7 +36,7 @@ describe("#authenticationProviders.info", () => {
it("should require authorization", async () => {
const team = await buildTeam();
const user = await buildUser();
const authenticationProviders = await team.getAuthenticationProviders();
const authenticationProviders = await team.$get("authenticationProviders");
const res = await server.post("/api/authenticationProviders.info", {
body: {
id: authenticationProviders[0].id,
@@ -48,7 +48,7 @@ describe("#authenticationProviders.info", () => {
it("should require authentication", async () => {
const team = await buildTeam();
const authenticationProviders = await team.getAuthenticationProviders();
const authenticationProviders = await team.$get("authenticationProviders");
const res = await server.post("/api/authenticationProviders.info", {
body: {
id: authenticationProviders[0].id,
@@ -57,13 +57,14 @@ describe("#authenticationProviders.info", () => {
expect(res.status).toEqual(401);
});
});
describe("#authenticationProviders.update", () => {
it("should not allow admins to disable when last authentication provider", async () => {
const team = await buildTeam();
const user = await buildAdmin({
teamId: team.id,
});
const authenticationProviders = await team.getAuthenticationProviders();
const authenticationProviders = await team.$get("authenticationProviders");
const res = await server.post("/api/authenticationProviders.update", {
body: {
id: authenticationProviders[0].id,
@@ -79,11 +80,11 @@ describe("#authenticationProviders.update", () => {
const user = await buildAdmin({
teamId: team.id,
});
await team.createAuthenticationProvider({
await team.$create("authenticationProvider", {
name: "google",
providerId: uuidv4(),
});
const authenticationProviders = await team.getAuthenticationProviders();
const authenticationProviders = await team.$get("authenticationProviders");
const res = await server.post("/api/authenticationProviders.update", {
body: {
id: authenticationProviders[0].id,
@@ -103,7 +104,7 @@ describe("#authenticationProviders.update", () => {
const user = await buildUser({
teamId: team.id,
});
const authenticationProviders = await team.getAuthenticationProviders();
const authenticationProviders = await team.$get("authenticationProviders");
const res = await server.post("/api/authenticationProviders.update", {
body: {
id: authenticationProviders[0].id,
@@ -116,7 +117,7 @@ describe("#authenticationProviders.update", () => {
it("should require authentication", async () => {
const team = await buildTeam();
const authenticationProviders = await team.getAuthenticationProviders();
const authenticationProviders = await team.$get("authenticationProviders");
const res = await server.post("/api/authenticationProviders.update", {
body: {
id: authenticationProviders[0].id,
@@ -126,6 +127,7 @@ describe("#authenticationProviders.update", () => {
expect(res.status).toEqual(401);
});
});
describe("#authenticationProviders.list", () => {
it("should return enabled and available auth providers", async () => {
const team = await buildTeam();

View File

@@ -1,24 +1,23 @@
import Router from "koa-router";
import auth from "@server/middlewares/authentication";
import { AuthenticationProvider, Event } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import {
presentAuthenticationProvider,
presentPolicies,
} from "@server/presenters";
import { assertUuid, assertPresent } from "@server/validation";
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'allAuthenticationProviders' implicitly h... Remove this comment to see the full error message
import allAuthenticationProviders from "../auth/providers";
const router = new Router();
const { authorize } = policy;
router.post("authenticationProviders.info", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const authenticationProvider = await AuthenticationProvider.findByPk(id);
authorize(user, "read", authenticationProvider);
ctx.body = {
data: presentAuthenticationProvider(authenticationProvider),
policies: presentPolicies(user, [authenticationProvider]),
@@ -29,7 +28,7 @@ router.post("authenticationProviders.update", auth(), async (ctx) => {
const { id, isEnabled } = ctx.body;
assertUuid(id, "id is required");
assertPresent(isEnabled, "isEnabled is required");
const user = ctx.state.user;
const { user } = ctx.state;
const authenticationProvider = await AuthenticationProvider.findByPk(id);
authorize(user, "update", authenticationProvider);
const enabled = !!isEnabled;
@@ -50,6 +49,7 @@ router.post("authenticationProviders.update", auth(), async (ctx) => {
actorId: user.id,
ip: ctx.request.ip,
});
ctx.body = {
data: presentAuthenticationProvider(authenticationProvider),
policies: presentPolicies(user, [authenticationProvider]),
@@ -57,10 +57,13 @@ router.post("authenticationProviders.update", auth(), async (ctx) => {
});
router.post("authenticationProviders.list", auth(), async (ctx) => {
const user = ctx.state.user;
const { user } = ctx.state;
authorize(user, "read", user.team);
const teamAuthenticationProviders = await user.team.getAuthenticationProviders();
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'allAuthenticationProviders' implicitly h... Remove this comment to see the full error message
const teamAuthenticationProviders = await user.team.$get(
"authenticationProviders"
);
const otherAuthenticationProviders = allAuthenticationProviders.filter(
(p) =>
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 't' implicitly has an 'any' type.
@@ -69,6 +72,7 @@ router.post("authenticationProviders.list", auth(), async (ctx) => {
// wants to be here in the future we'll need to migrate more data though
p.id !== "email"
);
ctx.body = {
data: {
authenticationProviders: [

View File

@@ -15,6 +15,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#collections.list", () => {
it("should require authentication", async () => {
const res = await server.post("/api/collections.list");
@@ -93,12 +94,12 @@ describe("#collections.list", () => {
const group = await buildGroup({
teamId: user.teamId,
});
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: user.id,
},
});
await collection.addGroup(group, {
await collection.$add("group", group, {
through: {
permission: "read",
createdById: user.id,
@@ -116,6 +117,7 @@ describe("#collections.list", () => {
expect(body.policies[0].abilities.read).toEqual(true);
});
});
describe("#collections.import", () => {
it("should error if no attachmentId is passed", async () => {
const user = await buildUser();
@@ -134,6 +136,7 @@ describe("#collections.import", () => {
expect(body).toMatchSnapshot();
});
});
describe("#collections.move", () => {
it("should require authentication", async () => {
const res = await server.post("/api/collections.move");
@@ -265,6 +268,7 @@ describe("#collections.move", () => {
expect(movedCollectionC.data.index < "b").toBeTruthy();
});
});
describe("#collections.export", () => {
it("should not allow export of private collection not a member", async () => {
const { admin } = await seed();
@@ -309,12 +313,12 @@ describe("#collections.export", () => {
const group = await buildGroup({
teamId: admin.teamId,
});
await group.addUser(admin, {
await group.$add("user", admin, {
through: {
createdById: admin.id,
},
});
await collection.addGroup(group, {
await collection.$add("group", group, {
through: {
permission: "read_write",
createdById: admin.id,
@@ -364,6 +368,7 @@ describe("#collections.export", () => {
expect(body.data.fileOperation.state).toBe("creating");
});
});
describe("#collections.export_all", () => {
it("should require authentication", async () => {
const res = await server.post("/api/collections.export_all");
@@ -392,6 +397,7 @@ describe("#collections.export_all", () => {
expect(res.status).toEqual(200);
});
});
describe("#collections.add_user", () => {
it("should add user to collection", async () => {
const user = await buildUser();
@@ -410,7 +416,7 @@ describe("#collections.add_user", () => {
userId: anotherUser.id,
},
});
const users = await collection.getUsers();
const users = await collection.$get("users");
expect(res.status).toEqual(200);
expect(users.length).toEqual(2);
});
@@ -455,6 +461,7 @@ describe("#collections.add_user", () => {
expect(res.status).toEqual(403);
});
});
describe("#collections.add_group", () => {
it("should add group to collection", async () => {
const user = await buildAdmin();
@@ -473,7 +480,7 @@ describe("#collections.add_group", () => {
groupId: group.id,
},
});
const groups = await collection.getGroups();
const groups = await collection.$get("groups");
expect(groups.length).toEqual(1);
expect(res.status).toEqual(200);
});
@@ -519,6 +526,7 @@ describe("#collections.add_group", () => {
expect(res.status).toEqual(403);
});
});
describe("#collections.remove_group", () => {
it("should remove group from collection", async () => {
const user = await buildAdmin();
@@ -537,7 +545,7 @@ describe("#collections.remove_group", () => {
groupId: group.id,
},
});
let users = await collection.getGroups();
let users = await collection.$get("groups");
expect(users.length).toEqual(1);
const res = await server.post("/api/collections.remove_group", {
body: {
@@ -546,7 +554,7 @@ describe("#collections.remove_group", () => {
groupId: group.id,
},
});
users = await collection.getGroups();
users = await collection.$get("groups");
expect(res.status).toEqual(200);
expect(users.length).toEqual(0);
});
@@ -591,6 +599,7 @@ describe("#collections.remove_group", () => {
expect(res.status).toEqual(403);
});
});
describe("#collections.remove_user", () => {
it("should remove user from collection", async () => {
const user = await buildUser();
@@ -616,7 +625,7 @@ describe("#collections.remove_user", () => {
userId: anotherUser.id,
},
});
const users = await collection.getUsers();
const users = await collection.$get("users");
expect(res.status).toEqual(200);
expect(users.length).toEqual(1);
});
@@ -661,6 +670,7 @@ describe("#collections.remove_user", () => {
expect(res.status).toEqual(403);
});
});
describe("#collections.users", () => {
it("should return users in private collection", async () => {
const { collection, user } = await seed();
@@ -702,6 +712,7 @@ describe("#collections.users", () => {
expect(res.status).toEqual(403);
});
});
describe("#collections.group_memberships", () => {
it("should return groups in private collection", async () => {
const user = await buildUser();
@@ -850,6 +861,7 @@ describe("#collections.group_memberships", () => {
expect(res.status).toEqual(403);
});
});
describe("#collections.memberships", () => {
it("should return members in private collection", async () => {
const { collection, user } = await seed();
@@ -952,6 +964,7 @@ describe("#collections.memberships", () => {
expect(res.status).toEqual(403);
});
});
describe("#collections.info", () => {
it("should return collection", async () => {
const { user, collection } = await seed();
@@ -1019,6 +1032,7 @@ describe("#collections.info", () => {
expect(res.status).toEqual(403);
});
});
describe("#collections.create", () => {
it("should require authentication", async () => {
const res = await server.post("/api/collections.create");
@@ -1163,6 +1177,7 @@ describe("#collections.create", () => {
expect(createdCollection.data.index < "b").toBeTruthy();
});
});
describe("#collections.update", () => {
it("should require authentication", async () => {
const collection = await buildCollection();
@@ -1317,12 +1332,12 @@ describe("#collections.update", () => {
const group = await buildGroup({
teamId: user.teamId,
});
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: user.id,
},
});
await collection.addGroup(group, {
await collection.$add("group", group, {
through: {
permission: "read_write",
createdById: user.id,
@@ -1393,6 +1408,7 @@ describe("#collections.update", () => {
expect(res.status).toEqual(400);
});
});
describe("#collections.delete", () => {
it("should require authentication", async () => {
const res = await server.post("/api/collections.delete");
@@ -1484,12 +1500,12 @@ describe("#collections.delete", () => {
const group = await buildGroup({
teamId: user.teamId,
});
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: user.id,
},
});
await collection.addGroup(group, {
await collection.$add("group", group, {
through: {
permission: "read_write",
createdById: user.id,

View File

@@ -1,5 +1,7 @@
import fractionalIndex from "fractional-index";
import invariant from "invariant";
import Router from "koa-router";
import { Sequelize, Op, WhereOptions } from "sequelize";
import collectionExporter from "@server/commands/collectionExporter";
import { ValidationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
@@ -13,7 +15,7 @@ import {
Group,
Attachment,
} from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import {
presentCollection,
presentUser,
@@ -23,7 +25,6 @@ import {
presentCollectionGroupMembership,
presentFileOperation,
} from "@server/presenters";
import { Op, sequelize } from "@server/sequelize";
import collectionIndexing from "@server/utils/collectionIndexing";
import removeIndexCollision from "@server/utils/removeIndexCollision";
import {
@@ -35,7 +36,6 @@ import {
} from "@server/validation";
import pagination from "./middlewares/pagination";
const { authorize } = policy;
const router = new Router();
router.post("collections.create", auth(), async (ctx) => {
@@ -55,7 +55,7 @@ router.post("collections.create", auth(), async (ctx) => {
assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
}
const user = ctx.state.user;
const { user } = ctx.state;
authorize(user, "createCollection", user.team);
if (index) {
@@ -70,7 +70,7 @@ router.post("collections.create", auth(), async (ctx) => {
limit: 1,
order: [
// using LC_COLLATE:"C" because we need byte order to drive the sorting
sequelize.literal('"collection"."index" collate "C"'),
Sequelize.literal('"collection"."index" collate "C"'),
["updatedAt", "DESC"],
],
});
@@ -82,7 +82,7 @@ router.post("collections.create", auth(), async (ctx) => {
}
index = await removeIndexCollision(user.teamId, index);
let collection = await Collection.create({
const collection = await Collection.create({
name,
description,
icon,
@@ -105,23 +105,27 @@ router.post("collections.create", auth(), async (ctx) => {
ip: ctx.request.ip,
});
// we must reload the collection to get memberships for policy presenter
collection = await Collection.scope({
const reloaded = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collection.id);
invariant(reloaded, "collection not found");
ctx.body = {
data: presentCollection(collection),
policies: presentPolicies(user, [collection]),
data: presentCollection(reloaded),
policies: presentPolicies(user, [reloaded]),
};
});
router.post("collections.info", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "read", collection);
ctx.body = {
data: presentCollection(collection),
policies: presentPolicies(user, [collection]),
@@ -132,7 +136,7 @@ router.post("collections.import", auth(), async (ctx) => {
const { type, attachmentId } = ctx.body;
assertIn(type, ["outline"], "type must be one of 'outline'");
assertUuid(attachmentId, "attachmentId is required");
const user = ctx.state.user;
const { user } = ctx.state;
authorize(user, "importCollection", user.team);
const attachment = await Attachment.findByPk(attachmentId);
authorize(user, "read", attachment);
@@ -146,6 +150,7 @@ router.post("collections.import", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
@@ -155,12 +160,15 @@ router.post("collections.add_group", auth(), async (ctx) => {
const { id, groupId, permission = "read_write" } = ctx.body;
assertUuid(id, "id is required");
assertUuid(groupId, "groupId is required");
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(ctx.state.user, "read", group);
let membership = await CollectionGroup.findOne({
where: {
collectionId: id,
@@ -191,6 +199,7 @@ router.post("collections.add_group", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
data: {
collectionGroupMemberships: [
@@ -204,13 +213,16 @@ router.post("collections.remove_group", auth(), async (ctx) => {
const { id, groupId } = ctx.body;
assertUuid(id, "id is required");
assertUuid(groupId, "groupId is required");
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(ctx.state.user, "read", group);
await collection.removeGroup(group);
await collection.$remove("group", group);
await Event.create({
name: "collections.remove_group",
collectionId: collection.id,
@@ -222,6 +234,7 @@ router.post("collections.remove_group", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
@@ -234,12 +247,14 @@ router.post(
async (ctx) => {
const { id, query, permission } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "read", collection);
let where = {
let where: WhereOptions<CollectionGroup> = {
collectionId: id,
};
let groupWhere;
@@ -253,7 +268,6 @@ router.post(
}
if (permission) {
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ permission: any; collectionId: any; }' is ... Remove this comment to see the full error message
where = { ...where, permission };
}
@@ -277,7 +291,6 @@ router.post(
collectionGroupMemberships: memberships.map(
presentCollectionGroupMembership
),
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'membership' implicitly has an 'any' typ... Remove this comment to see the full error message
groups: memberships.map((membership) => presentGroup(membership.group)),
},
};
@@ -288,12 +301,15 @@ router.post("collections.add_user", auth(), async (ctx) => {
const { id, userId, permission = "read_write" } = ctx.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
let membership = await CollectionUser.findOne({
where: {
collectionId: id,
@@ -324,6 +340,7 @@ router.post("collections.add_user", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
data: {
users: [presentUser(user)],
@@ -336,13 +353,16 @@ router.post("collections.remove_user", auth(), async (ctx) => {
const { id, userId } = ctx.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
await collection.removeUser(user);
await collection.$remove("user", user);
await Event.create({
name: "collections.remove_user",
userId,
@@ -354,34 +374,41 @@ router.post("collections.remove_user", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
});
// 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.user;
const { user } = ctx.state;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "read", collection);
const users = await collection.getUsers();
const users = await collection.$get("users");
ctx.body = {
data: users.map(presentUser),
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");
const user = ctx.state.user;
const { user } = ctx.state;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "read", collection);
let where = {
let where: WhereOptions<CollectionUser> = {
collectionId: id,
};
let userWhere;
@@ -395,7 +422,6 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => {
}
if (permission) {
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ permission: any; collectionId: any; }' is ... Remove this comment to see the full error message
where = { ...where, permission };
}
@@ -413,11 +439,11 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => {
},
],
});
ctx.body = {
pagination: ctx.state.pagination,
data: {
memberships: memberships.map(presentMembership),
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'membership' implicitly has an 'any' typ... Remove this comment to see the full error message
users: memberships.map((membership) => presentUser(membership.user)),
},
};
@@ -426,20 +452,22 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => {
router.post("collections.export", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
authorize(user, "export", team);
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
assertPresent(collection, "Collection should be present");
authorize(user, "read", collection);
const fileOperation = await collectionExporter({
collection,
user,
team,
ip: ctx.request.ip,
});
ctx.body = {
success: true,
data: {
@@ -449,14 +477,16 @@ router.post("collections.export", auth(), async (ctx) => {
});
router.post("collections.export_all", auth(), async (ctx) => {
const user = ctx.state.user;
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
authorize(user, "export", team);
const fileOperation = await collectionExporter({
user,
team,
ip: ctx.request.ip,
});
ctx.body = {
success: true,
data: {
@@ -481,7 +511,7 @@ router.post("collections.update", auth(), async (ctx) => {
assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
}
const user = ctx.state.user;
const { user } = ctx.state;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
@@ -580,7 +610,7 @@ router.post("collections.update", auth(), async (ctx) => {
});
router.post("collections.list", auth(), pagination(), async (ctx) => {
const user = ctx.state.user;
const { user } = ctx.state;
const collectionIds = await user.collectionIds();
const collections = await Collection.scope({
method: ["withMembership", user.id],
@@ -594,13 +624,11 @@ router.post("collections.list", auth(), pagination(), async (ctx) => {
limit: ctx.state.pagination.limit,
});
const nullIndexCollection = collections.findIndex(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'collection' implicitly has an 'any' typ... Remove this comment to see the full error message
(collection) => collection.index === null
);
if (nullIndexCollection !== -1) {
const indexedCollections = await collectionIndexing(ctx.state.user.teamId);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'collection' implicitly has an 'any' typ... Remove this comment to see the full error message
collections.forEach((collection) => {
collection.index = indexedCollections[collection.id];
});
@@ -615,7 +643,7 @@ router.post("collections.list", auth(), pagination(), async (ctx) => {
router.post("collections.delete", auth(), async (ctx) => {
const { id } = ctx.body;
const user = ctx.state.user;
const { user } = ctx.state;
assertUuid(id, "id is required");
const collection = await Collection.scope({
@@ -637,6 +665,7 @@ router.post("collections.delete", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
@@ -648,9 +677,11 @@ router.post("collections.move", auth(), async (ctx) => {
assertPresent(index, "index is required");
assertIndexCharacters(index);
assertUuid(id, "id must be a uuid");
const user = ctx.state.user;
const { user } = ctx.state;
const collection = await Collection.findByPk(id);
authorize(user, "move", collection);
index = await removeIndexCollision(user.teamId, index);
await collection.update({
index,
@@ -665,6 +696,7 @@ router.post("collections.move", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
data: {

View File

@@ -23,6 +23,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#documents.info", () => {
it("should return published document", async () => {
const { user, document } = await seed();
@@ -190,7 +191,7 @@ describe("#documents.info", () => {
expect(body.data.document.id).toEqual(childDocument.id);
expect(body.data.document.createdBy).toEqual(undefined);
expect(body.data.document.updatedBy).toEqual(undefined);
expect(body.data.sharedTree).toEqual(collection.documentStructure[0]);
expect(body.data.sharedTree).toEqual(collection.documentStructure?.[0]);
await share.reload();
expect(share.lastAccessedAt).toBeTruthy();
});
@@ -457,6 +458,7 @@ describe("#documents.info", () => {
expect(res.status).toEqual(400);
});
});
describe("#documents.export", () => {
it("should return published document", async () => {
const { user, document } = await seed();
@@ -656,6 +658,7 @@ describe("#documents.export", () => {
expect(res.status).toEqual(400);
});
});
describe("#documents.list", () => {
it("should return documents", async () => {
const { user, document } = await seed();
@@ -875,6 +878,7 @@ describe("#documents.drafts", () => {
expect(body.data.length).toEqual(0);
});
});
describe("#documents.search_titles", () => {
it("should return case insensitive results for partial query", async () => {
const user = await buildUser();
@@ -925,6 +929,7 @@ describe("#documents.search_titles", () => {
expect(res.status).toEqual(401);
});
});
describe("#documents.search", () => {
it("should return results", async () => {
const { user } = await seed();
@@ -1278,8 +1283,7 @@ describe("#documents.search", () => {
expect(body).toMatchSnapshot();
});
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(done: DoneCallback) => Promise<... Remove this comment to see the full error message
it("should save search term, hits and source", async (done) => {
it("should save search term, hits and source", async () => {
const { user } = await seed();
await server.post("/api/documents.search", {
body: {
@@ -1287,21 +1291,25 @@ describe("#documents.search", () => {
query: "my term",
},
});
// setTimeout is needed here because SearchQuery is saved asynchronously
// in order to not slow down the response time.
setTimeout(async () => {
const searchQuery = await SearchQuery.findAll({
where: {
query: "my term",
},
});
expect(searchQuery.length).toBe(1);
expect(searchQuery[0].results).toBe(0);
expect(searchQuery[0].source).toBe("app");
done();
}, 100);
return new Promise((resolve) => {
// setTimeout is needed here because SearchQuery is saved asynchronously
// in order to not slow down the response time.
setTimeout(async () => {
const searchQuery = await SearchQuery.findAll({
where: {
query: "my term",
},
});
expect(searchQuery.length).toBe(1);
expect(searchQuery[0].results).toBe(0);
expect(searchQuery[0].source).toBe("app");
resolve(undefined);
}, 100);
});
});
});
describe("#documents.archived", () => {
it("should return archived documents", async () => {
const { user } = await seed();
@@ -1326,7 +1334,7 @@ describe("#documents.archived", () => {
userId: user.id,
teamId: user.teamId,
});
await document.delete();
await document.delete(user.id);
const res = await server.post("/api/documents.archived", {
body: {
token: user.getJwtToken(),
@@ -1362,6 +1370,7 @@ describe("#documents.archived", () => {
expect(res.status).toEqual(401);
});
});
describe("#documents.viewed", () => {
it("should return empty result if no views", async () => {
const { user } = await seed();
@@ -1377,7 +1386,7 @@ describe("#documents.viewed", () => {
it("should return recently viewed documents", async () => {
const { user, document } = await seed();
await View.increment({
await View.incrementOrCreate({
documentId: document.id,
userId: user.id,
});
@@ -1395,7 +1404,7 @@ describe("#documents.viewed", () => {
it("should not return recently viewed but deleted documents", async () => {
const { user, document } = await seed();
await View.increment({
await View.incrementOrCreate({
documentId: document.id,
userId: user.id,
});
@@ -1412,7 +1421,7 @@ describe("#documents.viewed", () => {
it("should not return recently viewed documents in collection not a member of", async () => {
const { user, document, collection } = await seed();
await View.increment({
await View.incrementOrCreate({
documentId: document.id,
userId: user.id,
});
@@ -1435,6 +1444,7 @@ describe("#documents.viewed", () => {
expect(body).toMatchSnapshot();
});
});
describe("#documents.starred", () => {
it("should return empty result if no stars", async () => {
const { user } = await seed();
@@ -1524,10 +1534,11 @@ describe("#documents.move", () => {
expect(res.status).toEqual(403);
});
});
describe("#documents.restore", () => {
it("should allow restore of trashed documents", async () => {
const { user, document } = await seed();
await document.destroy(user.id);
await document.destroy();
const res = await server.post("/api/documents.restore", {
body: {
token: user.getJwtToken(),
@@ -1545,7 +1556,7 @@ describe("#documents.restore", () => {
userId: user.id,
teamId: user.teamId,
});
await document.destroy(user.id);
await document.destroy();
const res = await server.post("/api/documents.restore", {
body: {
token: user.getJwtToken(),
@@ -1561,7 +1572,7 @@ describe("#documents.restore", () => {
it("should not allow restore of documents in deleted collection", async () => {
const { user, document, collection } = await seed();
await document.destroy(user.id);
await document.destroy();
await collection.destroy();
const res = await server.post("/api/documents.restore", {
body: {
@@ -1575,7 +1586,7 @@ describe("#documents.restore", () => {
it("should not allow restore of trashed documents to collection user cannot access", async () => {
const { user, document } = await seed();
const collection = await buildCollection();
await document.destroy(user.id);
await document.destroy();
const res = await server.post("/api/documents.restore", {
body: {
token: user.getJwtToken(),
@@ -1749,6 +1760,7 @@ describe("#documents.star", () => {
expect(res.status).toEqual(403);
});
});
describe("#documents.unstar", () => {
it("should unstar the document", async () => {
const { user, document } = await seed();
@@ -1786,6 +1798,7 @@ describe("#documents.unstar", () => {
expect(res.status).toEqual(403);
});
});
describe("#documents.import", () => {
it("should error if no file is passed", async () => {
const user = await buildUser();
@@ -1807,6 +1820,7 @@ describe("#documents.import", () => {
expect(res.status).toEqual(401);
});
});
describe("#documents.create", () => {
it("should create as a new document", async () => {
const { user, collection } = await seed();
@@ -1822,8 +1836,8 @@ describe("#documents.create", () => {
const body = await res.json();
const newDocument = await Document.findByPk(body.data.id);
expect(res.status).toEqual(200);
expect(newDocument.parentDocumentId).toBe(null);
expect(newDocument.collectionId).toBe(collection.id);
expect(newDocument!.parentDocumentId).toBe(null);
expect(newDocument!.collectionId).toBe(collection.id);
expect(body.policies[0].abilities.update).toEqual(true);
});
@@ -1892,6 +1906,7 @@ describe("#documents.create", () => {
expect(body.policies[0].abilities.update).toEqual(true);
});
});
describe("#documents.update", () => {
it("should update document details in the root", async () => {
const { user, document } = await seed();
@@ -1901,7 +1916,7 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revision,
lastRevision: document.revisionCount,
},
});
const body = await res.json();
@@ -1929,7 +1944,7 @@ describe("#documents.update", () => {
id: template.id,
title: "Updated title",
text: "Updated text",
lastRevision: template.revision,
lastRevision: template.revisionCount,
publish: true,
},
});
@@ -1960,7 +1975,7 @@ describe("#documents.update", () => {
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revision,
lastRevision: document.revisionCount,
publish: true,
},
});
@@ -1974,14 +1989,14 @@ describe("#documents.update", () => {
it("should not edit archived document", async () => {
const { user, document } = await seed();
await document.archive();
await document.archive(user.id);
const res = await server.post("/api/documents.update", {
body: {
token: user.getJwtToken(),
id: document.id,
title: "Updated title",
text: "Updated text",
lastRevision: document.revision,
lastRevision: document.revisionCount,
},
});
expect(res.status).toEqual(403);
@@ -2048,7 +2063,7 @@ describe("#documents.update", () => {
token: admin.getJwtToken(),
id: document.id,
text: "Changed text",
lastRevision: document.revision,
lastRevision: document.revisionCount,
},
});
const body = await res.json();
@@ -2072,7 +2087,7 @@ describe("#documents.update", () => {
token: user.getJwtToken(),
id: document.id,
text: "Changed text",
lastRevision: document.revision,
lastRevision: document.revisionCount,
},
});
expect(res.status).toEqual(403);
@@ -2087,7 +2102,7 @@ describe("#documents.update", () => {
token: user.getJwtToken(),
id: document.id,
text: "Changed text",
lastRevision: document.revision,
lastRevision: document.revisionCount,
},
});
expect(res.status).toEqual(403);
@@ -2100,7 +2115,7 @@ describe("#documents.update", () => {
token: user.getJwtToken(),
id: document.id,
text: "Additional text",
lastRevision: document.revision,
lastRevision: document.revisionCount,
append: true,
},
});
@@ -2116,7 +2131,7 @@ describe("#documents.update", () => {
body: {
token: user.getJwtToken(),
id: document.id,
lastRevision: document.revision,
lastRevision: document.revisionCount,
title: "Updated Title",
append: true,
},
@@ -2132,7 +2147,7 @@ describe("#documents.update", () => {
body: {
token: user.getJwtToken(),
id: document.id,
lastRevision: document.revision,
lastRevision: document.revisionCount,
title: "Updated Title",
text: "",
},
@@ -2148,7 +2163,7 @@ describe("#documents.update", () => {
body: {
token: user.getJwtToken(),
id: document.id,
lastRevision: document.revision,
lastRevision: document.revisionCount,
title: document.title,
text: document.text,
},
@@ -2184,6 +2199,7 @@ describe("#documents.update", () => {
expect(res.status).toEqual(403);
});
});
describe("#documents.archive", () => {
it("should allow archiving document", async () => {
const { user, document } = await seed();
@@ -2209,6 +2225,7 @@ describe("#documents.archive", () => {
expect(res.status).toEqual(401);
});
});
describe("#documents.delete", () => {
it("should allow deleting document", async () => {
const { user, document } = await seed();
@@ -2251,6 +2268,7 @@ describe("#documents.delete", () => {
const { user, document, collection } = await seed();
// delete collection without hooks to trigger document deletion
await collection.destroy({
// @ts-expect-error type is incorrect here
hooks: false,
});
const res = await server.post("/api/documents.delete", {
@@ -2276,6 +2294,7 @@ describe("#documents.delete", () => {
expect(body).toMatchSnapshot();
});
});
describe("#documents.unpublish", () => {
it("should unpublish a document", async () => {
const { user, document } = await seed();
@@ -2291,12 +2310,12 @@ describe("#documents.unpublish", () => {
expect(body.data.publishedAt).toBeNull();
const reloaded = await Document.unscoped().findByPk(document.id);
expect(reloaded.userId).toEqual(user.id);
expect(reloaded!.createdById).toEqual(user.id);
});
it("should unpublish another users document", async () => {
const { user, collection } = await seed();
let document = await buildDocument({
const document = await buildDocument({
teamId: user.teamId,
collectionId: collection.id,
});
@@ -2310,8 +2329,9 @@ describe("#documents.unpublish", () => {
expect(res.status).toEqual(200);
expect(body.data.id).toEqual(document.id);
expect(body.data.publishedAt).toBeNull();
document = await Document.unscoped().findByPk(document.id);
expect(document.userId).toEqual(user.id);
const reloaded = await Document.unscoped().findByPk(document.id);
expect(reloaded!.createdById).toEqual(user.id);
});
it("should fail to unpublish a draft document", async () => {
@@ -2329,7 +2349,7 @@ describe("#documents.unpublish", () => {
it("should fail to unpublish a deleted document", async () => {
const { user, document } = await seed();
await document.delete();
await document.delete(user.id);
const res = await server.post("/api/documents.unpublish", {
body: {
token: user.getJwtToken(),
@@ -2341,7 +2361,7 @@ describe("#documents.unpublish", () => {
it("should fail to unpublish an archived document", async () => {
const { user, document } = await seed();
await document.archive();
await document.archive(user.id);
const res = await server.post("/api/documents.unpublish", {
body: {
token: user.getJwtToken(),

View File

@@ -1,5 +1,6 @@
import invariant from "invariant";
import Router from "koa-router";
import Sequelize from "sequelize";
import { Op, ScopeOptions, WhereOptions } from "sequelize";
import { subtractDate } from "@shared/utils/date";
import documentCreator from "@server/commands/documentCreator";
import documentImporter from "@server/commands/documentImporter";
@@ -24,13 +25,12 @@ import {
View,
Team,
} from "@server/models";
import policy from "@server/policies";
import { authorize, cannot, can } from "@server/policies";
import {
presentCollection,
presentDocument,
presentPolicies,
} from "@server/presenters";
import { sequelize } from "@server/sequelize";
import {
assertUuid,
assertSort,
@@ -41,8 +41,6 @@ import {
import env from "../../env";
import pagination from "./middlewares/pagination";
const Op = Sequelize.Op;
const { authorize, cannot, can } = policy;
const router = new Router();
router.post("documents.list", auth(), pagination(), async (ctx) => {
@@ -54,16 +52,15 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
let direction = ctx.body.direction;
if (direction !== "ASC") direction = "DESC";
// always filter by the current team
const user = ctx.state.user;
let where = {
const { user } = ctx.state;
let where: WhereOptions<Document> = {
teamId: user.teamId,
archivedAt: {
[Op.eq]: null,
[Op.is]: null,
},
};
if (template) {
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ template: boolean; teamId: any; archivedAt... Remove this comment to see the full error message
where = { ...where, template: true };
}
@@ -71,17 +68,14 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
// exist in the team then nothing will be returned, so no need to check auth
if (createdById) {
assertUuid(createdById, "user must be a UUID");
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ createdById: any; teamId: any; archivedAt:... Remove this comment to see the full error message
where = { ...where, createdById };
}
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'documentIds' implicitly has type 'any[]'... Remove this comment to see the full error message
let documentIds = [];
let documentIds: string[] = [];
// if a specific collection is passed then we need to check auth to view it
if (collectionId) {
assertUuid(collectionId, "collection must be a UUID");
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ collectionId: any; teamId: any; archivedAt... Remove this comment to see the full error message
where = { ...where, collectionId };
const collection = await Collection.scope({
method: ["withMembership", user.id],
@@ -91,22 +85,18 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
// index sort is special because it uses the order of the documents in the
// collection.documentStructure rather than a database column
if (sort === "index") {
documentIds = (collection.documentStructure || [])
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'node' implicitly has an 'any' type.
documentIds = (collection?.documentStructure || [])
.map((node) => node.id)
.slice(ctx.state.pagination.offset, ctx.state.pagination.limit);
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ id: any; teamId: any; archivedAt: { [Seque... Remove this comment to see the full error message
where = { ...where, id: documentIds };
} // otherwise, filter by all collections the user has access to
} else {
const collectionIds = await user.collectionIds();
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ collectionId: any; teamId: any; archivedAt... Remove this comment to see the full error message
where = { ...where, collectionId: collectionIds };
}
if (parentDocumentId) {
assertUuid(parentDocumentId, "parentDocumentId must be a UUID");
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ parentDocumentId: any; teamId: any; archiv... Remove this comment to see the full error message
where = { ...where, parentDocumentId };
}
@@ -115,9 +105,8 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
if (parentDocumentId === null) {
where = {
...where,
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ parentDocumentId: { [Sequelize.Op.eq]: nul... Remove this comment to see the full error message
parentDocumentId: {
[Op.eq]: null,
[Op.is]: null,
},
};
}
@@ -132,7 +121,6 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
});
where = {
...where,
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ id: any; teamId: any; archivedAt: { [Seque... Remove this comment to see the full error message
id: backlinks.map((backlink) => backlink.reverseDocumentId),
};
}
@@ -154,13 +142,11 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
// collection.documentStructure rather than a database column
if (documentIds.length) {
documents.sort(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'a' implicitly has an 'any' type.
(a, b) => documentIds.indexOf(a.id) - documentIds.indexOf(b.id)
);
}
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.map((document) => presentDocument(document))
);
const policies = presentPolicies(user, documents);
@@ -177,19 +163,19 @@ router.post("documents.archived", auth(), pagination(), async (ctx) => {
assertSort(sort, Document);
let direction = ctx.body.direction;
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const { user } = ctx.state;
const collectionIds = await user.collectionIds();
const collectionScope = {
const collectionScope: Readonly<ScopeOptions> = {
method: ["withCollection", user.id],
};
const viewScope = {
const viewScope: Readonly<ScopeOptions> = {
method: ["withViews", user.id],
};
const documents = await Document.scope(
const documents = await Document.scope([
"defaultScope",
collectionScope,
viewScope
).findAll({
viewScope,
]).findAll({
where: {
teamId: user.teamId,
collectionId: collectionIds,
@@ -202,10 +188,10 @@ router.post("documents.archived", auth(), pagination(), async (ctx) => {
limit: ctx.state.pagination.limit,
});
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.map((document) => presentDocument(document))
);
const policies = presentPolicies(user, documents);
ctx.body = {
pagination: ctx.state.pagination,
data,
@@ -219,17 +205,17 @@ router.post("documents.deleted", auth(), pagination(), async (ctx) => {
assertSort(sort, Document);
let direction = ctx.body.direction;
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const { user } = ctx.state;
const collectionIds = await user.collectionIds({
paranoid: false,
});
const collectionScope = {
const collectionScope: Readonly<ScopeOptions> = {
method: ["withCollection", user.id],
};
const viewScope = {
const viewScope: Readonly<ScopeOptions> = {
method: ["withViews", user.id],
};
const documents = await Document.scope(collectionScope, viewScope).findAll({
const documents = await Document.scope([collectionScope, viewScope]).findAll({
where: {
teamId: user.teamId,
collectionId: collectionIds,
@@ -255,10 +241,10 @@ router.post("documents.deleted", auth(), pagination(), async (ctx) => {
limit: ctx.state.pagination.limit,
});
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.map((document) => presentDocument(document))
);
const policies = presentPolicies(user, documents);
ctx.body = {
pagination: ctx.state.pagination,
data,
@@ -272,7 +258,7 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
assertSort(sort, Document);
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const { user } = ctx.state;
const collectionIds = await user.collectionIds();
const userId = user.id;
const views = await View.findAll({
@@ -309,17 +295,16 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'view' implicitly has an 'any' type.
const documents = views.map((view) => {
const document = view.document;
document.views = [view];
return document;
});
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.map((document) => presentDocument(document))
);
const policies = presentPolicies(user, documents);
ctx.body = {
pagination: ctx.state.pagination,
data,
@@ -333,7 +318,7 @@ router.post("documents.starred", auth(), pagination(), async (ctx) => {
assertSort(sort, Document);
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const { user } = ctx.state;
const collectionIds = await user.collectionIds();
const stars = await Star.findAll({
where: {
@@ -366,13 +351,13 @@ router.post("documents.starred", auth(), pagination(), async (ctx) => {
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'star' implicitly has an 'any' type.
const documents = stars.map((star) => star.document);
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.map((document) => presentDocument(document))
);
const policies = presentPolicies(user, documents);
ctx.body = {
pagination: ctx.state.pagination,
data,
@@ -386,7 +371,7 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => {
assertSort(sort, Document);
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const { user } = ctx.state;
if (collectionId) {
assertUuid(collectionId, "collectionId must be a UUID");
@@ -399,13 +384,12 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => {
const collectionIds = collectionId
? [collectionId]
: await user.collectionIds();
const whereConditions = {
userId: user.id,
const where: WhereOptions<Document> = {
createdById: user.id,
collectionId: collectionIds,
publishedAt: {
[Op.eq]: null,
[Op.is]: null,
},
updatedAt: undefined,
};
if (dateFilter) {
@@ -414,31 +398,30 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => {
["day", "week", "month", "year"],
"dateFilter must be one of day,week,month,year"
);
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ [Sequelize.Op.gte]: Date; }' is not assign... Remove this comment to see the full error message
whereConditions.updatedAt = {
where.updatedAt = {
[Op.gte]: subtractDate(new Date(), dateFilter),
};
} else {
delete whereConditions.updatedAt;
delete where.updatedAt;
}
const collectionScope = {
const collectionScope: Readonly<ScopeOptions> = {
method: ["withCollection", user.id],
};
const documents = await Document.scope(
const documents = await Document.scope([
"defaultScope",
collectionScope
).findAll({
where: whereConditions,
collectionScope,
]).findAll({
where,
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.map((document) => presentDocument(document))
);
const policies = presentPolicies(user, documents);
ctx.body = {
pagination: ctx.state.pagination,
data,
@@ -447,17 +430,16 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => {
});
async function loadDocument({
// @ts-expect-error ts-migrate(7031) FIXME: Binding element 'id' implicitly has an 'any' type.
id,
// @ts-expect-error ts-migrate(7031) FIXME: Binding element 'shareId' implicitly has an 'any' ... Remove this comment to see the full error message
shareId,
// @ts-expect-error ts-migrate(7031) FIXME: Binding element 'user' implicitly has an 'any' typ... Remove this comment to see the full error message
user,
}: {
id?: string;
shareId?: string;
user: User;
}): Promise<{
document: Document;
// @ts-expect-error ts-migrate(2749) FIXME: 'Share' refers to a value, but is being used as a ... Remove this comment to see the full error message
share?: Share;
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
collection: Collection;
}> {
let document;
@@ -468,7 +450,7 @@ async function loadDocument({
share = await Share.findOne({
where: {
revokedAt: {
[Op.eq]: null,
[Op.is]: null,
},
id: shareId,
},
@@ -518,6 +500,8 @@ async function loadDocument({
document = share.document;
}
invariant(document, "document not found");
// If the user has access to read the document, we can just update
// the last access date and return the document without additional checks.
const canReadDocument = can(user, "read", document);
@@ -542,6 +526,7 @@ async function loadDocument({
// It is possible to disable sharing at the collection so we must check
collection = await Collection.findByPk(document.collectionId);
invariant(collection, "collection not found");
if (!collection.sharing) {
throw AuthorizationError();
@@ -561,6 +546,7 @@ async function loadDocument({
// It is possible to disable sharing at the team level so we must check
const team = await Team.findByPk(document.teamId);
invariant(team, "team not found");
if (!team.sharing) {
throw AuthorizationError();
@@ -570,7 +556,7 @@ async function loadDocument({
lastAccessedAt: new Date(),
});
} else {
document = await Document.findByPk(id, {
document = await Document.findByPk(id as string, {
userId: user ? user.id : undefined,
paranoid: false,
});
@@ -641,14 +627,13 @@ router.post(
async (ctx) => {
const { id, shareId } = ctx.body;
assertPresent(id || shareId, "id or shareId is required");
const user = ctx.state.user;
const { user } = ctx.state;
const { document } = await loadDocument({
id,
shareId,
user,
});
ctx.body = {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'toMarkdown' does not exist on type 'Docu... Remove this comment to see the full error message
data: document.toMarkdown(),
};
}
@@ -657,7 +642,7 @@ router.post(
router.post("documents.restore", auth(), async (ctx) => {
const { id, collectionId, revisionId } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(id, {
userId: user.id,
paranoid: false,
@@ -722,7 +707,9 @@ router.post("documents.restore", auth(), async (ctx) => {
// restore a document to a specific revision
authorize(user, "update", document);
const revision = await Revision.findByPk(revisionId);
authorize(document, "restore", revision);
document.text = revision.text;
document.title = revision.title;
await document.save();
@@ -750,25 +737,25 @@ router.post("documents.restore", auth(), async (ctx) => {
router.post("documents.search_titles", auth(), pagination(), async (ctx) => {
const { query } = ctx.body;
const { offset, limit } = ctx.state.pagination;
const user = ctx.state.user;
const { user } = ctx.state;
assertPresent(query, "query is required");
const collectionIds = await user.collectionIds();
const documents = await Document.scope(
const documents = await Document.scope([
{
method: ["withViews", user.id],
},
{
method: ["withCollection", user.id],
}
).findAll({
},
]).findAll({
where: {
title: {
[Op.iLike]: `%${query}%`,
},
collectionId: collectionIds,
archivedAt: {
[Op.eq]: null,
[Op.is]: null,
},
},
order: [["updatedAt", "DESC"]],
@@ -789,9 +776,9 @@ router.post("documents.search_titles", auth(), pagination(), async (ctx) => {
});
const policies = presentPolicies(user, documents);
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
documents.map((document) => presentDocument(document))
);
ctx.body = {
pagination: ctx.state.pagination,
data,
@@ -809,7 +796,7 @@ router.post("documents.search", auth(), pagination(), async (ctx) => {
dateFilter,
} = ctx.body;
const { offset, limit } = ctx.state.pagination;
const user = ctx.state.user;
const { user } = ctx.state;
assertPresent(query, "query is required");
@@ -845,10 +832,10 @@ router.post("documents.search", auth(), pagination(), async (ctx) => {
offset,
limit,
});
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'result' implicitly has an 'any' type.
const documents = results.map((result) => result.document);
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'result' implicitly has an 'any' type.
results.map(async (result) => {
const document = await presentDocument(result.document);
return { ...result, document };
@@ -868,6 +855,7 @@ router.post("documents.search", auth(), pagination(), async (ctx) => {
}
const policies = presentPolicies(user, documents);
ctx.body = {
pagination: ctx.state.pagination,
data,
@@ -878,17 +866,20 @@ router.post("documents.search", auth(), pagination(), async (ctx) => {
router.post("documents.star", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(id, {
userId: user.id,
});
authorize(user, "read", document);
await Star.findOrCreate({
where: {
documentId: document.id,
userId: user.id,
},
});
await Event.create({
name: "documents.star",
documentId: document.id,
@@ -900,6 +891,7 @@ router.post("documents.star", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
@@ -908,11 +900,13 @@ router.post("documents.star", auth(), async (ctx) => {
router.post("documents.unstar", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(id, {
userId: user.id,
});
authorize(user, "read", document);
await Star.destroy({
where: {
documentId: document.id,
@@ -930,6 +924,7 @@ router.post("documents.unstar", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
@@ -938,12 +933,14 @@ router.post("documents.unstar", auth(), async (ctx) => {
router.post("documents.templatize", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const original = await Document.findByPk(id, {
userId: user.id,
});
authorize(user, "update", original);
let document = await Document.create({
const document = await Document.create({
editorVersion: original.editorVersion,
collectionId: original.collectionId,
teamId: original.teamId,
@@ -967,13 +964,16 @@ router.post("documents.templatize", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
// reload to get all of the data needed to present (user, collection etc)
document = await Document.findByPk(document.id, {
const reloaded = await Document.findByPk(document.id, {
userId: user.id,
});
invariant(reloaded, "document not found");
ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),
data: await presentDocument(reloaded),
policies: presentPolicies(user, [reloaded]),
};
});
@@ -990,11 +990,12 @@ router.post("documents.update", auth(), async (ctx) => {
templateId,
append,
} = ctx.body;
const editorVersion = ctx.headers["x-editor-version"];
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
assertPresent(id, "id is required");
assertPresent(title || text, "title or text is required");
if (append) assertPresent(text, "Text is required while appending");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(id, {
userId: user.id,
});
@@ -1026,7 +1027,7 @@ router.post("documents.update", auth(), async (ctx) => {
let transaction;
try {
transaction = await sequelize.transaction();
transaction = await document.sequelize.transaction();
if (publish) {
await document.publish(user.id, {
@@ -1034,7 +1035,6 @@ router.post("documents.update", auth(), async (ctx) => {
});
} else {
await document.save({
autosave,
transaction,
});
}
@@ -1093,6 +1093,7 @@ router.post("documents.update", auth(), async (ctx) => {
document.updatedBy = user;
document.collection = collection;
ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),
@@ -1118,11 +1119,12 @@ router.post("documents.move", auth(), async (ctx) => {
);
}
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(id, {
userId: user.id,
});
authorize(user, "move", document);
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collectionId);
@@ -1143,6 +1145,7 @@ router.post("documents.move", auth(), async (ctx) => {
index,
ip: ctx.request.ip,
});
ctx.body = {
data: {
documents: await Promise.all(
@@ -1159,11 +1162,13 @@ router.post("documents.move", auth(), async (ctx) => {
router.post("documents.archive", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(id, {
userId: user.id,
});
authorize(user, "archive", document);
await document.archive(user.id);
await Event.create({
name: "documents.archive",
@@ -1176,6 +1181,7 @@ router.post("documents.archive", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),
@@ -1185,7 +1191,7 @@ router.post("documents.archive", auth(), async (ctx) => {
router.post("documents.delete", auth(), async (ctx) => {
const { id, permanent } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
if (permanent) {
const document = await Document.findByPk(id, {
@@ -1193,6 +1199,7 @@ router.post("documents.delete", auth(), async (ctx) => {
paranoid: false,
});
authorize(user, "permanentDelete", document);
await Document.update(
{
parentDocumentId: null,
@@ -1220,7 +1227,9 @@ router.post("documents.delete", auth(), async (ctx) => {
const document = await Document.findByPk(id, {
userId: user.id,
});
authorize(user, "delete", document);
await document.delete(user.id);
await Event.create({
name: "documents.delete",
@@ -1243,11 +1252,13 @@ router.post("documents.delete", auth(), async (ctx) => {
router.post("documents.unpublish", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(id, {
userId: user.id,
});
authorize(user, "unpublish", document);
await document.unpublish(user.id);
await Event.create({
name: "documents.unpublish",
@@ -1260,6 +1271,7 @@ router.post("documents.unpublish", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),
@@ -1277,8 +1289,7 @@ router.post("documents.import", auth(), async (ctx) => {
const file: any = Object.values(ctx.request.files)[0];
assertPresent(file, "file is required");
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (file.size > env.MAXIMUM_IMPORT_SIZE) {
if (env.MAXIMUM_IMPORT_SIZE && file.size > env.MAXIMUM_IMPORT_SIZE) {
throw InvalidRequestError("The selected file was too large to import");
}
@@ -1289,8 +1300,9 @@ router.post("documents.import", auth(), async (ctx) => {
}
if (index) assertPositiveInteger(index, "index must be an integer (>=0)");
const user = ctx.state.user;
const { user } = ctx.state;
authorize(user, "createDocument", user.team);
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findOne({
@@ -1330,8 +1342,8 @@ router.post("documents.import", auth(), async (ctx) => {
user,
ip: ctx.request.ip,
});
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message
document.collection = collection;
return (ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),
@@ -1349,7 +1361,7 @@ router.post("documents.create", auth(), async (ctx) => {
template,
index,
} = ctx.body;
const editorVersion = ctx.headers["x-editor-version"];
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
assertUuid(collectionId, "collectionId must be an uuid");
if (parentDocumentId) {
@@ -1357,8 +1369,9 @@ router.post("documents.create", auth(), async (ctx) => {
}
if (index) assertPositiveInteger(index, "index must be an integer (>=0)");
const user = ctx.state.user;
const { user } = ctx.state;
authorize(user, "createDocument", user.team);
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findOne({
@@ -1368,6 +1381,7 @@ router.post("documents.create", auth(), async (ctx) => {
},
});
authorize(user, "publish", collection);
let parentDocument;
if (parentDocumentId) {
@@ -1401,12 +1415,11 @@ router.post("documents.create", auth(), async (ctx) => {
template,
index,
user,
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | string[] | undefined' is not assign... Remove this comment to see the full error message
editorVersion,
ip: ctx.request.ip,
});
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message
document.collection = collection;
return (ctx.body = {
data: await presentDocument(document),
policies: presentPolicies(user, [document]),

View File

@@ -8,6 +8,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#events.list", () => {
it("should only return activity events", async () => {
const { user, admin, document, collection } = await seed();

View File

@@ -1,18 +1,16 @@
import Router from "koa-router";
import Sequelize from "sequelize";
import { Op, WhereOptions } from "sequelize";
import auth from "@server/middlewares/authentication";
import { Event, User, Collection } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentEvent } from "@server/presenters";
import { assertSort, assertUuid } from "@server/validation";
import pagination from "./middlewares/pagination";
const Op = Sequelize.Op;
const { authorize } = policy;
const router = new Router();
router.post("events.list", auth(), pagination(), async (ctx) => {
const user = ctx.state.user;
const { user } = ctx.state;
let { direction } = ctx.body;
const {
sort = "createdAt",
@@ -24,27 +22,35 @@ router.post("events.list", auth(), pagination(), async (ctx) => {
} = ctx.body;
if (direction !== "ASC") direction = "DESC";
assertSort(sort, Event);
let where = {
let where: WhereOptions<Event> = {
name: Event.ACTIVITY_EVENTS,
teamId: user.teamId,
};
if (actorId) {
assertUuid(actorId, "actorId must be a UUID");
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ actorId: any; name: any; teamId: any; }' i... Remove this comment to see the full error message
where = { ...where, actorId };
}
if (documentId) {
assertUuid(documentId, "documentId must be a UUID");
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ documentId: any; name: any; teamId: any; }... Remove this comment to see the full error message
where = { ...where, documentId };
}
if (auditLog) {
authorize(user, "manage", user.team);
where.name = Event.AUDIT_EVENTS;
}
if (name && (where.name as string[]).includes(name)) {
where.name = name;
}
if (collectionId) {
assertUuid(collectionId, "collection must be a UUID");
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ collectionId: any; name: any; teamId: any;... Remove this comment to see the full error message
where = { ...where, collectionId };
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collectionId);
@@ -55,29 +61,19 @@ router.post("events.list", auth(), pagination(), async (ctx) => {
});
where = {
...where,
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ [Sequelize.Op.or]: { collectionId: any; }[... Remove this comment to see the full error message
[Op.or]: [
{
collectionId: collectionIds,
},
{
collectionId: {
[Op.eq]: null,
[Op.is]: null,
},
},
],
};
}
if (auditLog) {
authorize(user, "manage", user.team);
where.name = Event.AUDIT_EVENTS;
}
if (name && where.name.includes(name)) {
where.name = name;
}
const events = await Event.findAll({
where,
order: [[sort, direction]],
@@ -91,10 +87,12 @@ router.post("events.list", auth(), pagination(), async (ctx) => {
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
ctx.body = {
pagination: ctx.state.pagination,
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type.
data: events.map((event) => presentEvent(event, auditLog)),
data: await Promise.all(
events.map((event) => presentEvent(event, auditLog))
),
};
});

View File

@@ -13,19 +13,10 @@ import { flushdb } from "@server/test/support";
const app = webService();
const server = new TestServer(app.callback());
jest.mock("aws-sdk", () => {
const mS3 = {
createPresignedPost: jest.fn(),
deleteObject: jest.fn().mockReturnThis(),
promise: jest.fn(),
};
return {
S3: jest.fn(() => mS3),
Endpoint: jest.fn(),
};
});
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#fileOperations.info", () => {
it("should return fileOperation", async () => {
const team = await buildTeam();
@@ -73,6 +64,7 @@ describe("#fileOperations.info", () => {
expect(res.status).toEqual(403);
});
});
describe("#fileOperations.list", () => {
it("should return fileOperations list", async () => {
const team = await buildTeam();
@@ -212,6 +204,7 @@ describe("#fileOperations.list", () => {
expect(res.status).toEqual(403);
});
});
describe("#fileOperations.redirect", () => {
it("should not redirect when file operation is not complete", async () => {
const team = await buildTeam();
@@ -234,6 +227,7 @@ describe("#fileOperations.redirect", () => {
expect(body.message).toEqual("export is not complete yet");
});
});
describe("#fileOperations.info", () => {
it("should return file operation", async () => {
const team = await buildTeam();
@@ -279,6 +273,7 @@ describe("#fileOperations.info", () => {
expect(res.status).toBe(403);
});
});
describe("#fileOperations.delete", () => {
it("should delete file operation", async () => {
const team = await buildTeam();

View File

@@ -1,23 +1,26 @@
import invariant from "invariant";
import Router from "koa-router";
import { WhereOptions } from "sequelize/types";
import fileOperationDeleter from "@server/commands/fileOperationDeleter";
import { NotFoundError, ValidationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { FileOperation, Team } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentFileOperation } from "@server/presenters";
import { getSignedUrl } from "@server/utils/s3";
import { assertPresent, assertIn, assertUuid } from "@server/validation";
import pagination from "./middlewares/pagination";
const { authorize } = policy;
const router = new Router();
router.post("fileOperations.info", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
const fileOperation = await FileOperation.findByPk(id);
invariant(fileOperation, "File operation not found");
authorize(user, fileOperation.type, team);
if (!fileOperation) {
@@ -40,13 +43,14 @@ router.post("fileOperations.list", auth(), pagination(), async (ctx) => {
);
if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user;
const where = {
const { user } = ctx.state;
const where: WhereOptions<FileOperation> = {
teamId: user.teamId,
type,
};
const team = await Team.findByPk(user.teamId);
authorize(user, type, team);
const [exports, total] = await Promise.all([
await FileOperation.findAll({
where,
@@ -58,6 +62,7 @@ router.post("fileOperations.list", auth(), pagination(), async (ctx) => {
where,
}),
]);
ctx.body = {
pagination: { ...ctx.state.pagination, total },
data: exports.map(presentFileOperation),
@@ -68,7 +73,7 @@ router.post("fileOperations.redirect", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
const fileOp = await FileOperation.unscoped().findByPk(id);
@@ -90,7 +95,7 @@ router.post("fileOperations.delete", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
const fileOp = await FileOperation.findByPk(id);
@@ -100,6 +105,7 @@ router.post("fileOperations.delete", auth(), async (ctx) => {
authorize(user, fileOp.type, team);
await fileOperationDeleter(fileOp, user, ctx.request.ip);
ctx.body = {
success: true,
};

View File

@@ -9,6 +9,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#groups.create", () => {
it("should create a group", async () => {
const name = "hello I am a group";
@@ -24,6 +25,7 @@ describe("#groups.create", () => {
expect(body.data.name).toEqual(name);
});
});
describe("#groups.update", () => {
it("should require authentication", async () => {
const group = await buildGroup();
@@ -127,6 +129,7 @@ describe("#groups.update", () => {
});
});
});
describe("#groups.list", () => {
it("should require authentication", async () => {
const res = await server.post("/api/groups.list");
@@ -140,7 +143,7 @@ describe("#groups.list", () => {
const group = await buildGroup({
teamId: user.teamId,
});
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: user.id,
},
@@ -169,12 +172,12 @@ describe("#groups.list", () => {
const group = await buildGroup({
teamId: user.teamId,
});
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: me.id,
},
});
await group.addUser(me, {
await group.$add("user", me, {
through: {
createdById: me.id,
},
@@ -196,6 +199,7 @@ describe("#groups.list", () => {
expect(body.policies[0].abilities.read).toEqual(true);
});
});
describe("#groups.info", () => {
it("should return group if admin", async () => {
const user = await buildAdmin();
@@ -218,7 +222,7 @@ describe("#groups.info", () => {
const group = await buildGroup({
teamId: user.teamId,
});
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: user.id,
},
@@ -271,6 +275,7 @@ describe("#groups.info", () => {
expect(res.status).toEqual(403);
});
});
describe("#groups.delete", () => {
it("should require authentication", async () => {
const group = await buildGroup();
@@ -324,13 +329,14 @@ describe("#groups.delete", () => {
expect(body.success).toEqual(true);
});
});
describe("#groups.memberships", () => {
it("should return members in a group", async () => {
const user = await buildUser();
const group = await buildGroup({
teamId: user.teamId,
});
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: user.id,
},
@@ -361,17 +367,17 @@ describe("#groups.memberships", () => {
const group = await buildGroup({
teamId: user.teamId,
});
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: user.id,
},
});
await group.addUser(user2, {
await group.$add("user", user2, {
through: {
createdById: user.id,
},
});
await group.addUser(user3, {
await group.$add("user", user3, {
through: {
createdById: user.id,
},
@@ -409,6 +415,7 @@ describe("#groups.memberships", () => {
expect(res.status).toEqual(403);
});
});
describe("#groups.add_user", () => {
it("should add user to group", async () => {
const user = await buildAdmin();
@@ -422,7 +429,7 @@ describe("#groups.add_user", () => {
userId: user.id,
},
});
const users = await group.getUsers();
const users = await group.$get("users");
expect(res.status).toEqual(200);
expect(users.length).toEqual(1);
});
@@ -470,6 +477,7 @@ describe("#groups.add_user", () => {
expect(body).toMatchSnapshot();
});
});
describe("#groups.remove_user", () => {
it("should remove user from group", async () => {
const user = await buildAdmin();
@@ -483,7 +491,7 @@ describe("#groups.remove_user", () => {
userId: user.id,
},
});
const users = await group.getUsers();
const users = await group.$get("users");
expect(users.length).toEqual(1);
const res = await server.post("/api/groups.remove_user", {
body: {
@@ -492,7 +500,7 @@ describe("#groups.remove_user", () => {
userId: user.id,
},
});
const users1 = await group.getUsers();
const users1 = await group.$get("users");
expect(res.status).toEqual(200);
expect(users1.length).toEqual(0);
});

View File

@@ -1,19 +1,19 @@
import invariant from "invariant";
import Router from "koa-router";
import { Op } from "sequelize";
import { MAX_AVATAR_DISPLAY } from "@shared/constants";
import auth from "@server/middlewares/authentication";
import { User, Event, Group, GroupUser } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import {
presentGroup,
presentPolicies,
presentUser,
presentGroupMembership,
} from "@server/presenters";
import { Op } from "@server/sequelize";
import { assertPresent, assertUuid, assertSort } from "@server/validation";
import pagination from "./middlewares/pagination";
const { authorize } = policy;
const router = new Router();
router.post("groups.list", auth(), pagination(), async (ctx) => {
@@ -22,7 +22,7 @@ router.post("groups.list", auth(), pagination(), async (ctx) => {
if (direction !== "ASC") direction = "DESC";
assertSort(sort, Group);
const user = ctx.state.user;
const { user } = ctx.state;
const groups = await Group.findAll({
where: {
teamId: user.teamId,
@@ -37,10 +37,8 @@ router.post("groups.list", auth(), pagination(), async (ctx) => {
data: {
groups: groups.map(presentGroup),
groupMemberships: groups
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'g' implicitly has an 'any' type.
.map((g) =>
g.groupMemberships
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'membership' implicitly has an 'any' typ... Remove this comment to see the full error message
.filter((membership) => !!membership.user)
.slice(0, MAX_AVATAR_DISPLAY)
)
@@ -55,9 +53,10 @@ router.post("groups.info", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const group = await Group.findByPk(id);
authorize(user, "read", group);
ctx.body = {
data: presentGroup(group),
policies: presentPolicies(user, [group]),
@@ -68,15 +67,18 @@ router.post("groups.create", auth(), async (ctx) => {
const { name } = ctx.body;
assertPresent(name, "name is required");
const user = ctx.state.user;
const { user } = ctx.state;
authorize(user, "createGroup", user.team);
let group = await Group.create({
const g = await Group.create({
name,
teamId: user.teamId,
createdById: user.id,
});
// reload to get default scope
group = await Group.findByPk(group.id);
const group = await Group.findByPk(g.id);
invariant(group, "group not found");
await Event.create({
name: "groups.create",
actorId: user.id,
@@ -87,6 +89,7 @@ router.post("groups.create", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
data: presentGroup(group),
policies: presentPolicies(user, [group]),
@@ -98,9 +101,10 @@ router.post("groups.update", auth(), async (ctx) => {
assertPresent(name, "name is required");
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const group = await Group.findByPk(id);
authorize(user, "update", group);
group.name = name;
if (group.changed()) {
@@ -130,6 +134,7 @@ router.post("groups.delete", auth(), async (ctx) => {
const { user } = ctx.state;
const group = await Group.findByPk(id);
authorize(user, "delete", group);
await group.destroy();
await Event.create({
name: "groups.delete",
@@ -141,6 +146,7 @@ router.post("groups.delete", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
@@ -150,7 +156,7 @@ router.post("groups.memberships", auth(), pagination(), async (ctx) => {
const { id, query } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const group = await Group.findByPk(id);
authorize(user, "read", group);
let userWhere;
@@ -179,11 +185,11 @@ router.post("groups.memberships", auth(), pagination(), async (ctx) => {
},
],
});
ctx.body = {
pagination: ctx.state.pagination,
data: {
groupMemberships: memberships.map(presentGroupMembership),
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'membership' implicitly has an 'any' typ... Remove this comment to see the full error message
users: memberships.map((membership) => presentUser(membership.user)),
},
};
@@ -196,8 +202,10 @@ router.post("groups.add_user", auth(), async (ctx) => {
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
let group = await Group.findByPk(id);
authorize(ctx.state.user, "update", group);
let membership = await GroupUser.findOne({
where: {
groupId: id,
@@ -206,7 +214,7 @@ router.post("groups.add_user", auth(), async (ctx) => {
});
if (!membership) {
await group.addUser(user, {
await group.$add("user", user, {
through: {
createdById: ctx.state.user.id,
},
@@ -218,8 +226,12 @@ router.post("groups.add_user", auth(), async (ctx) => {
userId,
},
});
invariant(membership, "membership not found");
// reload to get default scope
group = await Group.findByPk(id);
invariant(group, "group not found");
await Event.create({
name: "groups.add_user",
userId,
@@ -249,9 +261,11 @@ router.post("groups.remove_user", auth(), async (ctx) => {
let group = await Group.findByPk(id);
authorize(ctx.state.user, "update", group);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
await group.removeUser(user);
await group.$remove("user", user);
await Event.create({
name: "groups.remove_user",
userId,
@@ -263,8 +277,11 @@ router.post("groups.remove_user", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
// reload to get default scope
group = await Group.findByPk(id);
invariant(group, "group not found");
ctx.body = {
data: {
groups: [presentGroup(group)],

View File

@@ -13,6 +13,7 @@ afterAll(() => server.close());
jest.mock("../../utils/slack", () => ({
post: jest.fn(),
}));
describe("#hooks.unfurl", () => {
it("should return documents", async () => {
const { user, document } = await seed();
@@ -45,6 +46,7 @@ describe("#hooks.unfurl", () => {
expect(Slack.post).toHaveBeenCalled();
});
});
describe("#hooks.slack", () => {
it("should return no matches", async () => {
const { user, team } = await seed();
@@ -126,8 +128,8 @@ describe("#hooks.slack", () => {
"This title *contains* a search term"
);
});
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(done: DoneCallback) => Promise<... Remove this comment to see the full error message
it("should save search term, hits and source", async (done) => {
it("should save search term, hits and source", async () => {
const { user, team } = await seed();
await server.post("/api/hooks.slack", {
body: {
@@ -137,19 +139,22 @@ describe("#hooks.slack", () => {
text: "contains",
},
});
// setTimeout is needed here because SearchQuery is saved asynchronously
// in order to not slow down the response time.
setTimeout(async () => {
const searchQuery = await SearchQuery.findAll({
where: {
query: "contains",
},
});
expect(searchQuery.length).toBe(1);
expect(searchQuery[0].results).toBe(0);
expect(searchQuery[0].source).toBe("slack");
done();
}, 100);
return new Promise((resolve) => {
// setTimeout is needed here because SearchQuery is saved asynchronously
// in order to not slow down the response time.
setTimeout(async () => {
const searchQuery = await SearchQuery.findAll({
where: {
query: "contains",
},
});
expect(searchQuery.length).toBe(1);
expect(searchQuery[0].results).toBe(0);
expect(searchQuery[0].source).toBe("slack");
resolve(undefined);
}, 100);
});
});
it("should respond with help content for help keyword", async () => {
@@ -259,6 +264,7 @@ describe("#hooks.slack", () => {
expect(res.status).toEqual(401);
});
});
describe("#hooks.interactive", () => {
it("should respond with replacement message", async () => {
const { user, team } = await seed();

View File

@@ -1,3 +1,4 @@
import invariant from "invariant";
import Router from "koa-router";
import { escapeRegExp } from "lodash";
import { AuthenticationError, InvalidRequestError } from "@server/errors";
@@ -93,6 +94,8 @@ router.post("hooks.interactive", async (ctx) => {
}
const team = await Team.findByPk(document.teamId);
invariant(team, "team not found");
// respond with a public message that will be posted in the original channel
ctx.body = {
response_type: "in_channel",

View File

@@ -7,12 +7,14 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("POST unknown endpoint", () => {
it("should be not found", async () => {
const res = await server.post("/api/blah");
expect(res.status).toEqual(404);
});
});
describe("GET unknown endpoint", () => {
it("should be not found", async () => {
const res = await server.get("/api/blah");

View File

@@ -2,12 +2,11 @@ import Router from "koa-router";
import auth from "@server/middlewares/authentication";
import { Event } from "@server/models";
import Integration from "@server/models/Integration";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentIntegration } from "@server/presenters";
import { assertSort, assertUuid, assertArray } from "@server/validation";
import pagination from "./middlewares/pagination";
const { authorize } = policy;
const router = new Router();
router.post("integrations.list", auth(), pagination(), async (ctx) => {
@@ -16,7 +15,7 @@ router.post("integrations.list", auth(), pagination(), async (ctx) => {
if (direction !== "ASC") direction = "DESC";
assertSort(sort, Integration);
const user = ctx.state.user;
const { user } = ctx.state;
const integrations = await Integration.findAll({
where: {
teamId: user.teamId,
@@ -25,6 +24,7 @@ router.post("integrations.list", auth(), pagination(), async (ctx) => {
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
ctx.body = {
pagination: ctx.state.pagination,
data: integrations.map(presentIntegration),

View File

@@ -7,6 +7,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#pagination", () => {
it("should allow offset and limit", async () => {
const { user } = await seed();

View File

@@ -1,18 +1,17 @@
import Router from "koa-router";
import auth from "@server/middlewares/authentication";
import { Team, NotificationSetting } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentNotificationSetting } from "@server/presenters";
import { assertPresent, assertUuid } from "@server/validation";
const { authorize } = policy;
const router = new Router();
router.post("notificationSettings.create", auth(), async (ctx) => {
const { event } = ctx.body;
assertPresent(event, "event is required");
const user = ctx.state.user;
const { user } = ctx.state;
authorize(user, "createNotificationSetting", user.team);
const [setting] = await NotificationSetting.findOrCreate({
where: {
@@ -21,18 +20,20 @@ router.post("notificationSettings.create", auth(), async (ctx) => {
event,
},
});
ctx.body = {
data: presentNotificationSetting(setting),
};
});
router.post("notificationSettings.list", auth(), async (ctx) => {
const user = ctx.state.user;
const { user } = ctx.state;
const settings = await NotificationSetting.findAll({
where: {
userId: user.id,
},
});
ctx.body = {
data: settings.map(presentNotificationSetting),
};
@@ -42,10 +43,12 @@ router.post("notificationSettings.delete", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const setting = await NotificationSetting.findByPk(id);
authorize(user, "delete", setting);
await setting.destroy();
ctx.body = {
success: true,
};

View File

@@ -1,20 +1,20 @@
import invariant from "invariant";
import Router from "koa-router";
import { Sequelize, Op } from "sequelize";
import pinCreator from "@server/commands/pinCreator";
import pinDestroyer from "@server/commands/pinDestroyer";
import pinUpdater from "@server/commands/pinUpdater";
import auth from "@server/middlewares/authentication";
import { Collection, Document, Pin } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import {
presentPin,
presentDocument,
presentPolicies,
} from "@server/presenters";
import { sequelize, Op } from "@server/sequelize";
import { assertUuid, assertIndexCharacters } from "@server/validation";
import pagination from "./middlewares/pagination";
const { authorize } = policy;
const router = new Router();
router.post("pins.create", auth(), async (ctx) => {
@@ -65,11 +65,11 @@ router.post("pins.list", auth(), pagination(), async (ctx) => {
where: {
...(collectionId
? { collectionId }
: { collectionId: { [Op.eq]: null } }),
: { collectionId: { [Op.is]: null } }),
teamId: user.teamId,
},
order: [
sequelize.literal('"pins"."index" collate "C"'),
Sequelize.literal('"pin"."index" collate "C"'),
["updatedAt", "DESC"],
],
offset: ctx.state.pagination.offset,
@@ -80,7 +80,7 @@ router.post("pins.list", auth(), pagination(), async (ctx) => {
const documents = await Document.defaultScopeWithUser(user.id).findAll({
where: {
id: pins.map((pin: any) => pin.documentId),
id: pins.map((pin) => pin.documentId),
collectionId: collectionIds,
},
});
@@ -92,7 +92,7 @@ router.post("pins.list", auth(), pagination(), async (ctx) => {
data: {
pins: pins.map(presentPin),
documents: await Promise.all(
documents.map((document: any) => presentDocument(document))
documents.map((document: Document) => presentDocument(document))
),
},
policies,
@@ -107,6 +107,8 @@ router.post("pins.update", auth(), async (ctx) => {
const { user } = ctx.state;
let pin = await Pin.findByPk(id);
invariant(pin, "pin not found");
const document = await Document.findByPk(pin.documentId, {
userId: user.id,
});
@@ -136,6 +138,8 @@ router.post("pins.delete", auth(), async (ctx) => {
const { user } = ctx.state;
const pin = await Pin.findByPk(id);
invariant(pin, "pin not found");
const document = await Document.findByPk(pin.documentId, {
userId: user.id,
});

View File

@@ -9,6 +9,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#revisions.info", () => {
it("should return a document revision", async () => {
const { user, document } = await seed();
@@ -38,6 +39,7 @@ describe("#revisions.info", () => {
expect(res.status).toEqual(403);
});
});
describe("#revisions.list", () => {
it("should return a document's revisions", async () => {
const { user, document } = await seed();

View File

@@ -2,18 +2,17 @@ import Router from "koa-router";
import { NotFoundError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { Document, Revision } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentRevision } from "@server/presenters";
import { assertPresent, assertSort } from "@server/validation";
import pagination from "./middlewares/pagination";
const { authorize } = policy;
const router = new Router();
router.post("revisions.info", auth(), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const revision = await Revision.findByPk(id);
if (!revision) {
@@ -38,11 +37,12 @@ router.post("revisions.list", auth(), pagination(), async (ctx) => {
assertSort(sort, Revision);
assertPresent(documentId, "documentId is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(documentId, {
userId: user.id,
});
authorize(user, "read", document);
const revisions = await Revision.findAll({
where: {
documentId: document.id,
@@ -52,9 +52,9 @@ router.post("revisions.list", auth(), pagination(), async (ctx) => {
limit: ctx.state.pagination.limit,
});
const data = await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'revision' implicitly has an 'any' type.
revisions.map((revision) => presentRevision(revision))
);
ctx.body = {
pagination: ctx.state.pagination,
data,

View File

@@ -8,7 +8,7 @@ import pagination from "./middlewares/pagination";
const router = new Router();
router.post("searches.list", auth(), pagination(), async (ctx) => {
const user = ctx.state.user;
const { user } = ctx.state;
const searches = await SearchQuery.findAll({
where: {

View File

@@ -9,6 +9,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#shares.list", () => {
it("should only return shares created by user", async () => {
const { user, admin, document } = await seed();
@@ -133,6 +134,7 @@ describe("#shares.list", () => {
expect(body).toMatchSnapshot();
});
});
describe("#shares.create", () => {
it("should allow creating a share record for document", async () => {
const { user, document } = await seed();
@@ -183,7 +185,7 @@ describe("#shares.create", () => {
teamId: user.teamId,
userId: user.id,
});
await share.revoke();
await share.revoke(user.id);
const res = await server.post("/api/shares.create", {
body: {
token: user.getJwtToken(),
@@ -284,6 +286,7 @@ describe("#shares.create", () => {
expect(res.status).toEqual(403);
});
});
describe("#shares.info", () => {
it("should allow reading share by id", async () => {
const { user, document } = await seed();
@@ -375,7 +378,7 @@ describe("#shares.info", () => {
teamId: user.teamId,
userId: user.id,
});
await share.revoke();
await share.revoke(user.id);
const res = await server.post("/api/shares.info", {
body: {
token: user.getJwtToken(),
@@ -551,6 +554,7 @@ describe("#shares.info", () => {
expect(res.status).toEqual(403);
});
});
describe("#shares.update", () => {
it("should allow user to update a share", async () => {
const { user, document } = await seed();
@@ -647,6 +651,7 @@ describe("#shares.update", () => {
expect(res.status).toEqual(403);
});
});
describe("#shares.revoke", () => {
it("should allow author to revoke a share", async () => {
const { user, document } = await seed();

View File

@@ -1,22 +1,20 @@
import Router from "koa-router";
import Sequelize from "sequelize";
import { Op, WhereOptions } from "sequelize";
import { NotFoundError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { Document, User, Event, Share, Team, Collection } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentShare, presentPolicies } from "@server/presenters";
import { assertUuid, assertSort, assertPresent } from "@server/validation";
import pagination from "./middlewares/pagination";
const Op = Sequelize.Op;
const { authorize } = policy;
const router = new Router();
router.post("shares.info", auth(), async (ctx) => {
const { id, documentId, apiVersion } = ctx.body;
assertUuid(id || documentId, "id or documentId is required");
const user = ctx.state.user;
const { user } = ctx.state;
const shares = [];
const share = await Share.scope({
method: ["withCollection", user.id],
@@ -25,14 +23,14 @@ router.post("shares.info", auth(), async (ctx) => {
? {
id,
revokedAt: {
[Op.eq]: null,
[Op.is]: null,
},
}
: {
documentId,
teamId: user.teamId,
revokedAt: {
[Op.eq]: null,
[Op.is]: null,
},
},
});
@@ -72,7 +70,7 @@ router.post("shares.info", auth(), async (ctx) => {
documentId: parentIds,
teamId: user.teamId,
revokedAt: {
[Op.eq]: null,
[Op.is]: null,
},
includeChildDocuments: true,
published: true,
@@ -105,13 +103,13 @@ router.post("shares.list", auth(), pagination(), async (ctx) => {
if (direction !== "ASC") direction = "DESC";
assertSort(sort, Share);
const user = ctx.state.user;
const where = {
const { user } = ctx.state;
const where: WhereOptions<Share> = {
teamId: user.teamId,
userId: user.id,
published: true,
revokedAt: {
[Op.eq]: null,
[Op.is]: null,
},
};
@@ -155,9 +153,9 @@ router.post("shares.list", auth(), pagination(), async (ctx) => {
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
ctx.body = {
pagination: ctx.state.pagination,
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'share' implicitly has an 'any' type.
data: shares.map((share) => presentShare(share, user.isAdmin)),
policies: presentPolicies(user, shares),
};
@@ -174,6 +172,7 @@ router.post("shares.update", auth(), async (ctx) => {
const share = await Share.scope({
method: ["withCollection", user.id],
}).findByPk(id);
authorize(user, "update", share);
if (published !== undefined) {
@@ -203,6 +202,7 @@ router.post("shares.update", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
data: presentShare(share, user.isAdmin),
policies: presentPolicies(user, [share]),
@@ -213,14 +213,16 @@ router.post("shares.create", auth(), async (ctx) => {
const { documentId } = ctx.body;
assertPresent(documentId, "documentId is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(documentId, {
userId: user.id,
});
const team = await Team.findByPk(user.teamId);
// user could be creating the share link to share with team members
authorize(user, "read", document);
const team = await Team.findByPk(user.teamId);
const [share, isCreated] = await Share.findOrCreate({
where: {
documentId,
@@ -247,9 +249,12 @@ router.post("shares.create", auth(), async (ctx) => {
});
}
share.team = team;
if (team) {
share.team = team;
}
share.user = user;
share.document = document;
ctx.body = {
data: presentShare(share),
policies: presentPolicies(user, [share]),
@@ -260,15 +265,16 @@ router.post("shares.revoke", auth(), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const user = ctx.state.user;
const { user } = ctx.state;
const share = await Share.findByPk(id);
authorize(user, "revoke", share);
const document = await Document.findByPk(share.documentId);
if (!document) {
if (!share?.document) {
throw NotFoundError();
}
authorize(user, "revoke", share);
const { document } = share;
await share.revoke(user.id);
await Event.create({
name: "shares.revoke",
@@ -282,6 +288,7 @@ router.post("shares.revoke", auth(), async (ctx) => {
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};

View File

@@ -7,6 +7,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#team.update", () => {
it("should update team details", async () => {
const { admin } = await seed();

View File

@@ -1,10 +1,9 @@
import Router from "koa-router";
import auth from "@server/middlewares/authentication";
import { Event, Team } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentTeam, presentPolicies } from "@server/presenters";
const { authorize } = policy;
const router = new Router();
router.post("team.update", auth(), async (ctx) => {
@@ -18,7 +17,7 @@ router.post("team.update", auth(), async (ctx) => {
collaborativeEditing,
defaultUserRole,
} = ctx.body;
const user = ctx.state.user;
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
authorize(user, "update", team);

View File

@@ -8,6 +8,7 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#users.list", () => {
it("should allow filtering by user name", async () => {
const user = await buildUser({
@@ -103,6 +104,7 @@ describe("#users.list", () => {
expect(body.data[1].id).toEqual(admin.id);
});
});
describe("#users.info", () => {
it("should return current user with no id", async () => {
const user = await buildUser();
@@ -154,6 +156,7 @@ describe("#users.info", () => {
expect(res.status).toEqual(401);
});
});
describe("#users.invite", () => {
it("should return sent invites", async () => {
const user = await buildAdmin();
@@ -274,6 +277,7 @@ describe("#users.invite", () => {
expect(res.status).toEqual(401);
});
});
describe("#users.delete", () => {
it("should not allow deleting without confirmation", async () => {
const user = await buildUser();
@@ -352,6 +356,7 @@ describe("#users.delete", () => {
expect(body).toMatchSnapshot();
});
});
describe("#users.update", () => {
it("should update user profile information", async () => {
const { user } = await seed();
@@ -373,6 +378,7 @@ describe("#users.update", () => {
expect(body).toMatchSnapshot();
});
});
describe("#users.promote", () => {
it("should promote a new admin", async () => {
const { admin, user } = await seed();
@@ -400,6 +406,7 @@ describe("#users.promote", () => {
expect(body).toMatchSnapshot();
});
});
describe("#users.demote", () => {
it("should demote an admin", async () => {
const { admin, user } = await seed();
@@ -480,6 +487,7 @@ describe("#users.demote", () => {
expect(body).toMatchSnapshot();
});
});
describe("#users.suspend", () => {
it("should suspend an user", async () => {
const { admin, user } = await seed();
@@ -520,6 +528,7 @@ describe("#users.suspend", () => {
expect(body).toMatchSnapshot();
});
});
describe("#users.activate", () => {
it("should activate a suspended user", async () => {
const { admin, user } = await seed();
@@ -552,6 +561,7 @@ describe("#users.activate", () => {
expect(body).toMatchSnapshot();
});
});
describe("#users.count", () => {
it("should count active users", async () => {
const team = await buildTeam();

View File

@@ -1,12 +1,12 @@
import Router from "koa-router";
import { Op, WhereOptions } from "sequelize";
import userDestroyer from "@server/commands/userDestroyer";
import userInviter from "@server/commands/userInviter";
import userSuspender from "@server/commands/userSuspender";
import auth from "@server/middlewares/authentication";
import { Event, User, Team } from "@server/models";
import policy from "@server/policies";
import { can, authorize } from "@server/policies";
import { presentUser, presentPolicies } from "@server/presenters";
import { Op } from "@server/sequelize";
import {
assertIn,
assertSort,
@@ -15,7 +15,6 @@ import {
} from "@server/validation";
import pagination from "./middlewares/pagination";
const { can, authorize } = policy;
const router = new Router();
router.post("users.list", auth(), pagination(), async (ctx) => {
@@ -33,25 +32,22 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
}
const actor = ctx.state.user;
let where = {
let where: WhereOptions<User> = {
teamId: actor.teamId,
};
switch (filter) {
case "invited": {
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ lastActiveAt: null; teamId: any; }' is not... Remove this comment to see the full error message
where = { ...where, lastActiveAt: null };
break;
}
case "viewers": {
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ isViewer: boolean; teamId: any; }' is not ... Remove this comment to see the full error message
where = { ...where, isViewer: true };
break;
}
case "admins": {
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ isAdmin: boolean; teamId: any; }' is not a... Remove this comment to see the full error message
where = { ...where, isAdmin: true };
break;
}
@@ -59,7 +55,6 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
case "suspended": {
where = {
...where,
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ suspendedAt: { [Op.ne]: null; }; teamId: a... Remove this comment to see the full error message
suspendedAt: {
[Op.ne]: null,
},
@@ -74,9 +69,8 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
default: {
where = {
...where,
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ suspendedAt: { [Op.eq]: null; }; teamId: a... Remove this comment to see the full error message
suspendedAt: {
[Op.eq]: null,
[Op.is]: null,
},
};
break;
@@ -86,7 +80,6 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
if (query) {
where = {
...where,
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ name: { [Op.iLike]: string; }; teamId: any... Remove this comment to see the full error message
name: {
[Op.iLike]: `%${query}%`,
},
@@ -104,9 +97,9 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
where,
}),
]);
ctx.body = {
pagination: { ...ctx.state.pagination, total },
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
data: users.map((user) =>
presentUser(user, {
includeDetails: can(actor, "readDetails", user),
@@ -119,6 +112,7 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
router.post("users.count", auth(), async (ctx) => {
const { user } = ctx.state;
const counts = await User.getCounts(user.teamId);
ctx.body = {
data: {
counts,
@@ -132,6 +126,7 @@ router.post("users.info", auth(), async (ctx) => {
const user = id ? await User.findByPk(id) : actor;
authorize(actor, "read", user);
const includeDetails = can(actor, "readDetails", user);
ctx.body = {
data: presentUser(user, {
includeDetails,
@@ -154,12 +149,14 @@ router.post("users.update", auth(), async (ctx) => {
teamId: user.teamId,
ip: ctx.request.ip,
});
ctx.body = {
data: presentUser(user, {
includeDetails: true,
}),
};
});
// Admin specific
router.post("users.promote", auth(), async (ctx) => {
const userId = ctx.body.id;
@@ -168,6 +165,7 @@ router.post("users.promote", auth(), async (ctx) => {
assertPresent(userId, "id is required");
const user = await User.findByPk(userId);
authorize(actor, "promote", user);
await user.promote();
await Event.create({
name: "users.promote",
@@ -180,6 +178,7 @@ router.post("users.promote", auth(), async (ctx) => {
ip: ctx.request.ip,
});
const includeDetails = can(actor, "readDetails", user);
ctx.body = {
data: presentUser(user, {
includeDetails,
@@ -197,6 +196,7 @@ router.post("users.demote", auth(), async (ctx) => {
to = to === "viewer" ? "viewer" : "member";
const user = await User.findByPk(userId);
authorize(actor, "demote", user);
await user.demote(teamId, to);
await Event.create({
name: "users.demote",
@@ -209,6 +209,7 @@ router.post("users.demote", auth(), async (ctx) => {
ip: ctx.request.ip,
});
const includeDetails = can(actor, "readDetails", user);
ctx.body = {
data: presentUser(user, {
includeDetails,
@@ -223,12 +224,14 @@ router.post("users.suspend", auth(), async (ctx) => {
assertPresent(userId, "id is required");
const user = await User.findByPk(userId);
authorize(actor, "suspend", user);
await userSuspender({
user,
actorId: actor.id,
ip: ctx.request.ip,
});
const includeDetails = can(actor, "readDetails", user);
ctx.body = {
data: presentUser(user, {
includeDetails,
@@ -244,6 +247,7 @@ router.post("users.activate", auth(), async (ctx) => {
assertPresent(userId, "id is required");
const user = await User.findByPk(userId);
authorize(actor, "activate", user);
await user.activate();
await Event.create({
name: "users.activate",
@@ -256,6 +260,7 @@ router.post("users.activate", auth(), async (ctx) => {
ip: ctx.request.ip,
});
const includeDetails = can(actor, "readDetails", user);
ctx.body = {
data: presentUser(user, {
includeDetails,
@@ -270,11 +275,13 @@ router.post("users.invite", auth(), async (ctx) => {
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
authorize(user, "inviteUser", team);
const response = await userInviter({
user,
invites,
ip: ctx.request.ip,
});
ctx.body = {
data: {
sent: response.sent,
@@ -299,6 +306,7 @@ router.post("users.delete", auth(), async (ctx) => {
actor,
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};

View File

@@ -2,26 +2,16 @@ import { subDays } from "date-fns";
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'fetc... Remove this comment to see the full error message
import TestServer from "fetch-test-server";
import { Document, FileOperation } from "@server/models";
import { Op } from "@server/sequelize";
import webService from "@server/services/web";
import { buildDocument, buildFileOperation } from "@server/test/factories";
import { flushdb } from "@server/test/support";
const app = webService();
const server = new TestServer(app.callback());
jest.mock("aws-sdk", () => {
const mS3 = {
createPresignedPost: jest.fn(),
deleteObject: jest.fn().mockReturnThis(),
promise: jest.fn(),
};
return {
S3: jest.fn(() => mS3),
Endpoint: jest.fn(),
};
});
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#utils.gc", () => {
it("should not destroy documents not deleted", async () => {
await buildDocument({
@@ -112,9 +102,7 @@ describe("#utils.gc", () => {
const data = await FileOperation.count({
where: {
type: "export",
state: {
[Op.eq]: "expired",
},
state: "expired",
},
});
expect(res.status).toEqual(200);
@@ -139,9 +127,7 @@ describe("#utils.gc", () => {
const data = await FileOperation.count({
where: {
type: "export",
state: {
[Op.eq]: "expired",
},
state: "expired",
},
});
expect(res.status).toEqual(200);

View File

@@ -1,10 +1,10 @@
import { subDays } from "date-fns";
import Router from "koa-router";
import { Op } from "sequelize";
import documentPermanentDeleter from "@server/commands/documentPermanentDeleter";
import teamPermanentDeleter from "@server/commands/teamPermanentDeleter";
import { AuthenticationError } from "@server/errors";
import { Document, Team, FileOperation } from "@server/models";
import { Op } from "@server/sequelize";
import Logger from "../../logging/logger";
const router = new Router();
@@ -48,7 +48,6 @@ router.post("utils.gc", async (ctx) => {
},
});
await Promise.all(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'e' implicitly has an 'any' type.
exports.map(async (e) => {
await e.expire();
})

View File

@@ -9,10 +9,11 @@ const app = webService();
const server = new TestServer(app.callback());
beforeEach(() => flushdb());
afterAll(() => server.close());
describe("#views.list", () => {
it("should return views for a document", async () => {
const { user, document } = await seed();
await View.increment({
await View.incrementOrCreate({
documentId: document.id,
userId: user.id,
});
@@ -38,7 +39,7 @@ describe("#views.list", () => {
userId: user.id,
permission: "read",
});
await View.increment({
await View.incrementOrCreate({
documentId: document.id,
userId: user.id,
});
@@ -78,6 +79,7 @@ describe("#views.list", () => {
expect(res.status).toEqual(403);
});
});
describe("#views.create", () => {
it("should allow creating a view record for document", async () => {
const { user, document } = await seed();

View File

@@ -1,23 +1,23 @@
import Router from "koa-router";
import auth from "@server/middlewares/authentication";
import { View, Document, Event } from "@server/models";
import policy from "@server/policies";
import { authorize } from "@server/policies";
import { presentView } from "@server/presenters";
import { assertUuid } from "@server/validation";
const { authorize } = policy;
const router = new Router();
router.post("views.list", auth(), async (ctx) => {
const { documentId } = ctx.body;
assertUuid(documentId, "documentId is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(documentId, {
userId: user.id,
});
authorize(user, "read", document);
const views = await View.findByDocument(documentId);
ctx.body = {
data: views.map(presentView),
};
@@ -27,15 +27,17 @@ router.post("views.create", auth(), async (ctx) => {
const { documentId } = ctx.body;
assertUuid(documentId, "documentId is required");
const user = ctx.state.user;
const { user } = ctx.state;
const document = await Document.findByPk(documentId, {
userId: user.id,
});
authorize(user, "read", document);
const view = await View.increment({
const view = await View.incrementOrCreate({
documentId,
userId: user.id,
});
await Event.create({
name: "views.create",
actorId: user.id,
@@ -48,6 +50,7 @@ router.post("views.create", auth(), async (ctx) => {
ip: ctx.request.ip,
});
view.user = user;
ctx.body = {
data: presentView(view),
};