@@ -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,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user