chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { ApiKey, Event } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { presentApiKey } from "../../presenters";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { ApiKey, Event } from "@server/models";
|
||||
import policy from "@server/policies";
|
||||
import { presentApiKey } from "@server/presenters";
|
||||
import { assertUuid, assertPresent } from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const { authorize } = policy;
|
||||
@@ -12,25 +11,23 @@ const router = new Router();
|
||||
|
||||
router.post("apiKeys.create", auth(), async (ctx) => {
|
||||
const { name } = ctx.body;
|
||||
ctx.assertPresent(name, "name is required");
|
||||
|
||||
assertPresent(name, "name is required");
|
||||
const user = ctx.state.user;
|
||||
authorize(user, "createApiKey", user.team);
|
||||
|
||||
const key = await ApiKey.create({
|
||||
name,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await Event.create({
|
||||
name: "api_keys.create",
|
||||
modelId: key.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: { name },
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentApiKey(key),
|
||||
};
|
||||
@@ -46,7 +43,6 @@ 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),
|
||||
@@ -55,23 +51,21 @@ router.post("apiKeys.list", auth(), pagination(), async (ctx) => {
|
||||
|
||||
router.post("apiKeys.delete", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
|
||||
assertUuid(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const key = await ApiKey.findByPk(id);
|
||||
authorize(user, "delete", key);
|
||||
|
||||
await key.destroy();
|
||||
|
||||
await Event.create({
|
||||
name: "api_keys.delete",
|
||||
modelId: key.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: { name: key.name },
|
||||
data: {
|
||||
name: key.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -1,30 +1,30 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 "../../models";
|
||||
import webService from "../../services/web";
|
||||
import { Attachment } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
import {
|
||||
buildUser,
|
||||
buildAdmin,
|
||||
buildCollection,
|
||||
buildAttachment,
|
||||
buildDocument,
|
||||
} from "../../test/factories";
|
||||
import { flushdb } from "../../test/support";
|
||||
} 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 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
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");
|
||||
@@ -38,9 +38,11 @@ describe("#attachments.delete", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/attachments.delete", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
});
|
||||
@@ -51,14 +53,14 @@ describe("#attachments.delete", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
attachment.documentId = null;
|
||||
await attachment.save();
|
||||
|
||||
const res = await server.post("/api/attachments.delete", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
});
|
||||
@@ -68,14 +70,14 @@ describe("#attachments.delete", () => {
|
||||
const attachment = await buildAttachment({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
attachment.documentId = null;
|
||||
await attachment.save();
|
||||
|
||||
const res = await server.post("/api/attachments.delete", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
});
|
||||
@@ -83,14 +85,14 @@ describe("#attachments.delete", () => {
|
||||
it("should not allow deleting an attachment in another team", async () => {
|
||||
const user = await buildAdmin();
|
||||
const attachment = await buildAttachment();
|
||||
|
||||
attachment.documentId = null;
|
||||
await attachment.save();
|
||||
|
||||
const res = await server.post("/api/attachments.delete", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
@@ -99,14 +101,14 @@ describe("#attachments.delete", () => {
|
||||
const attachment = await buildAttachment({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
attachment.documentId = null;
|
||||
await attachment.save();
|
||||
|
||||
const res = await server.post("/api/attachments.delete", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
@@ -126,15 +128,15 @@ describe("#attachments.delete", () => {
|
||||
documentId: document.id,
|
||||
acl: "private",
|
||||
});
|
||||
|
||||
const res = await server.post("/api/attachments.delete", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#attachments.redirect", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/attachments.redirect");
|
||||
@@ -148,10 +150,12 @@ describe("#attachments.redirect", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/attachments.redirect", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
});
|
||||
|
||||
@@ -173,10 +177,12 @@ describe("#attachments.redirect", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/attachments.redirect", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
});
|
||||
|
||||
@@ -197,12 +203,13 @@ describe("#attachments.redirect", () => {
|
||||
userId: user.id,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/attachments.redirect", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
});
|
||||
|
||||
@@ -222,11 +229,12 @@ describe("#attachments.redirect", () => {
|
||||
documentId: document.id,
|
||||
acl: "private",
|
||||
});
|
||||
|
||||
const res = await server.post("/api/attachments.redirect", {
|
||||
body: { token: user.getJwtToken(), id: attachment.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: attachment.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +1,30 @@
|
||||
// @flow
|
||||
import { format } from "date-fns";
|
||||
import Router from "koa-router";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { NotFoundError } from "../../errors";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Attachment, Document, Event } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { NotFoundError } from "@server/errors";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Attachment, Document, Event } from "@server/models";
|
||||
import policy from "@server/policies";
|
||||
import {
|
||||
makePolicy,
|
||||
getSignature,
|
||||
publicS3Endpoint,
|
||||
makeCredential,
|
||||
getSignedUrl,
|
||||
} from "../../utils/s3";
|
||||
} 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";
|
||||
|
||||
router.post("attachments.create", auth(), async (ctx) => {
|
||||
let { name, documentId, contentType, size } = ctx.body;
|
||||
|
||||
ctx.assertPresent(name, "name is required");
|
||||
ctx.assertPresent(contentType, "contentType is required");
|
||||
ctx.assertPresent(size, "size is required");
|
||||
|
||||
const { name, documentId, contentType, size } = ctx.body;
|
||||
assertPresent(name, "name is required");
|
||||
assertPresent(contentType, "contentType is required");
|
||||
assertPresent(size, "size is required");
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "createAttachment", user.team);
|
||||
|
||||
const s3Key = uuidv4();
|
||||
const acl =
|
||||
ctx.body.public === undefined
|
||||
@@ -35,7 +32,6 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
: ctx.body.public
|
||||
? "public-read"
|
||||
: "private";
|
||||
|
||||
const bucket = acl === "public-read" ? "public" : "uploads";
|
||||
const key = `${bucket}/${user.id}/${s3Key}/${name}`;
|
||||
const credential = makeCredential();
|
||||
@@ -45,7 +41,9 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
const url = `${endpoint}/${key}`;
|
||||
|
||||
if (documentId) {
|
||||
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "update", document);
|
||||
}
|
||||
|
||||
@@ -59,15 +57,15 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await Event.create({
|
||||
name: "attachments.create",
|
||||
data: { name },
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE,
|
||||
@@ -96,13 +94,13 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
});
|
||||
|
||||
router.post("attachments.delete", auth(), async (ctx) => {
|
||||
let { id } = ctx.body;
|
||||
ctx.assertPresent(id, "id is required");
|
||||
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const attachment = await Attachment.findByPk(id);
|
||||
|
||||
if (!attachment) {
|
||||
throw new NotFoundError();
|
||||
throw NotFoundError();
|
||||
}
|
||||
|
||||
if (attachment.documentId) {
|
||||
@@ -114,14 +112,12 @@ router.post("attachments.delete", auth(), async (ctx) => {
|
||||
|
||||
authorize(user, "delete", attachment);
|
||||
await attachment.destroy();
|
||||
|
||||
await Event.create({
|
||||
name: "attachments.delete",
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -129,12 +125,12 @@ router.post("attachments.delete", auth(), async (ctx) => {
|
||||
|
||||
router.post("attachments.redirect", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, "id is required");
|
||||
|
||||
assertPresent(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const attachment = await Attachment.findByPk(id);
|
||||
|
||||
if (!attachment) {
|
||||
throw new NotFoundError();
|
||||
throw NotFoundError();
|
||||
}
|
||||
|
||||
if (attachment.isPrivate) {
|
||||
@@ -1,24 +1,25 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 webService from "../../services/web";
|
||||
import { buildUser, buildTeam } from "../../test/factories";
|
||||
import { flushdb } from "../../test/support";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
|
||||
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();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/api/auth.info", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.user.name).toBe(user.name);
|
||||
expect(body.data.team.name).toBe(team.name);
|
||||
@@ -26,12 +27,14 @@ describe("#auth.info", () => {
|
||||
|
||||
it("should require the team to not be deleted", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
await team.destroy();
|
||||
|
||||
const res = await server.post("/api/auth.info", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
@@ -41,12 +44,10 @@ 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");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.providers.length).toBe(2);
|
||||
expect(body.data.providers[0].name).toBe("Slack");
|
||||
@@ -55,7 +56,6 @@ describe("#auth.config", () => {
|
||||
|
||||
it("should return available providers for team subdomain", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
|
||||
await buildTeam({
|
||||
guestSignin: false,
|
||||
subdomain: "example",
|
||||
@@ -67,10 +67,11 @@ describe("#auth.config", () => {
|
||||
],
|
||||
});
|
||||
const res = await server.post("/api/auth.config", {
|
||||
headers: { host: `example.localoutline.com` },
|
||||
headers: {
|
||||
host: `example.localoutline.com`,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.providers.length).toBe(1);
|
||||
expect(body.data.providers[0].name).toBe("Slack");
|
||||
@@ -88,10 +89,11 @@ describe("#auth.config", () => {
|
||||
],
|
||||
});
|
||||
const res = await server.post("/api/auth.config", {
|
||||
headers: { host: "docs.mycompany.com" },
|
||||
headers: {
|
||||
host: "docs.mycompany.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.providers.length).toBe(1);
|
||||
expect(body.data.providers[0].name).toBe("Slack");
|
||||
@@ -99,7 +101,6 @@ describe("#auth.config", () => {
|
||||
|
||||
it("should return email provider for team when guest signin enabled", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
|
||||
await buildTeam({
|
||||
guestSignin: true,
|
||||
subdomain: "example",
|
||||
@@ -111,10 +112,11 @@ describe("#auth.config", () => {
|
||||
],
|
||||
});
|
||||
const res = await server.post("/api/auth.config", {
|
||||
headers: { host: "example.localoutline.com" },
|
||||
headers: {
|
||||
host: "example.localoutline.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.providers.length).toBe(2);
|
||||
expect(body.data.providers[0].name).toBe("Slack");
|
||||
@@ -123,7 +125,6 @@ describe("#auth.config", () => {
|
||||
|
||||
it("should not return provider when disabled", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
|
||||
await buildTeam({
|
||||
guestSignin: false,
|
||||
subdomain: "example",
|
||||
@@ -136,18 +137,17 @@ describe("#auth.config", () => {
|
||||
],
|
||||
});
|
||||
const res = await server.post("/api/auth.config", {
|
||||
headers: { host: "example.localoutline.com" },
|
||||
headers: {
|
||||
host: "example.localoutline.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.providers.length).toBe(0);
|
||||
});
|
||||
|
||||
describe("self hosted", () => {
|
||||
it("should return available providers for team", async () => {
|
||||
process.env.DEPLOYMENT = "";
|
||||
|
||||
await buildTeam({
|
||||
guestSignin: false,
|
||||
authenticationProviders: [
|
||||
@@ -159,15 +159,12 @@ describe("#auth.config", () => {
|
||||
});
|
||||
const res = await server.post("/api/auth.config");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.providers.length).toBe(1);
|
||||
expect(body.data.providers[0].name).toBe("Slack");
|
||||
});
|
||||
|
||||
it("should return email provider for team when guest signin enabled", async () => {
|
||||
process.env.DEPLOYMENT = "";
|
||||
|
||||
await buildTeam({
|
||||
guestSignin: true,
|
||||
authenticationProviders: [
|
||||
@@ -179,7 +176,6 @@ describe("#auth.config", () => {
|
||||
});
|
||||
const res = await server.post("/api/auth.config");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.providers.length).toBe(2);
|
||||
expect(body.data.providers[0].name).toBe("Slack");
|
||||
@@ -1,16 +1,18 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import { find } from "lodash";
|
||||
import { parseDomain, isCustomSubdomain } from "../../../shared/utils/domains";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Team } from "../../models";
|
||||
import { presentUser, presentTeam, presentPolicies } from "../../presenters";
|
||||
import { isCustomDomain } from "../../utils/domains";
|
||||
import { parseDomain, isCustomSubdomain } from "@shared/utils/domains";
|
||||
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
|
||||
return providers
|
||||
.sort((provider) => (provider.id === "email" ? 1 : -1))
|
||||
.filter((provider) => {
|
||||
@@ -22,7 +24,10 @@ function filterProviders(team) {
|
||||
|
||||
return (
|
||||
!team ||
|
||||
find(team.authenticationProviders, { name: provider.id, enabled: true })
|
||||
find(team.authenticationProviders, {
|
||||
name: provider.id,
|
||||
enabled: true,
|
||||
})
|
||||
);
|
||||
})
|
||||
.map((provider) => ({
|
||||
@@ -53,7 +58,9 @@ router.post("auth.config", async (ctx) => {
|
||||
|
||||
if (isCustomDomain(ctx.request.hostname)) {
|
||||
const team = await Team.scope("withAuthenticationProviders").findOne({
|
||||
where: { domain: ctx.request.hostname },
|
||||
where: {
|
||||
domain: ctx.request.hostname,
|
||||
},
|
||||
});
|
||||
|
||||
if (team) {
|
||||
@@ -78,7 +85,9 @@ router.post("auth.config", async (ctx) => {
|
||||
const domain = parseDomain(ctx.request.hostname);
|
||||
const subdomain = domain ? domain.subdomain : undefined;
|
||||
const team = await Team.scope("withAuthenticationProviders").findOne({
|
||||
where: { subdomain },
|
||||
where: {
|
||||
subdomain,
|
||||
},
|
||||
});
|
||||
|
||||
if (team) {
|
||||
@@ -96,6 +105,7 @@ router.post("auth.config", async (ctx) => {
|
||||
// Otherwise, we're requesting from the standard root signin page
|
||||
ctx.body = {
|
||||
data: {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
providers: filterProviders(),
|
||||
},
|
||||
};
|
||||
@@ -104,10 +114,11 @@ router.post("auth.config", async (ctx) => {
|
||||
router.post("auth.info", auth(), async (ctx) => {
|
||||
const user = ctx.state.user;
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
user: presentUser(user, { includeDetails: true }),
|
||||
user: presentUser(user, {
|
||||
includeDetails: true,
|
||||
}),
|
||||
team: presentTeam(team),
|
||||
},
|
||||
policies: presentPolicies(user, [team]),
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
// @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 { v4 as uuidv4 } from "uuid";
|
||||
import webService from "../../services/web";
|
||||
import { buildUser, buildAdmin, buildTeam } from "../../test/factories";
|
||||
import { flushdb } from "../../test/support";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildAdmin, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
|
||||
const app = webService();
|
||||
const server = new TestServer(app.callback());
|
||||
@@ -14,9 +14,10 @@ afterAll(() => server.close());
|
||||
describe("#authenticationProviders.info", () => {
|
||||
it("should return auth provider", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
|
||||
const res = await server.post("/api/authenticationProviders.info", {
|
||||
body: {
|
||||
id: authenticationProviders[0].id,
|
||||
@@ -24,7 +25,6 @@ describe("#authenticationProviders.info", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toBe("slack");
|
||||
expect(body.data.isEnabled).toBe(true);
|
||||
@@ -37,7 +37,6 @@ describe("#authenticationProviders.info", () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser();
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
|
||||
const res = await server.post("/api/authenticationProviders.info", {
|
||||
body: {
|
||||
id: authenticationProviders[0].id,
|
||||
@@ -50,7 +49,6 @@ describe("#authenticationProviders.info", () => {
|
||||
it("should require authentication", async () => {
|
||||
const team = await buildTeam();
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
|
||||
const res = await server.post("/api/authenticationProviders.info", {
|
||||
body: {
|
||||
id: authenticationProviders[0].id,
|
||||
@@ -59,13 +57,13 @@ 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 user = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
|
||||
const res = await server.post("/api/authenticationProviders.update", {
|
||||
body: {
|
||||
id: authenticationProviders[0].id,
|
||||
@@ -73,19 +71,19 @@ describe("#authenticationProviders.update", () => {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
|
||||
it("should allow admins to disable", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildAdmin({ teamId: team.id });
|
||||
const user = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
await team.createAuthenticationProvider({
|
||||
name: "google",
|
||||
providerId: uuidv4(),
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
|
||||
const res = await server.post("/api/authenticationProviders.update", {
|
||||
body: {
|
||||
id: authenticationProviders[0].id,
|
||||
@@ -94,7 +92,6 @@ describe("#authenticationProviders.update", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toBe("slack");
|
||||
expect(body.data.isEnabled).toBe(false);
|
||||
@@ -103,9 +100,10 @@ describe("#authenticationProviders.update", () => {
|
||||
|
||||
it("should require authorization", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
|
||||
const res = await server.post("/api/authenticationProviders.update", {
|
||||
body: {
|
||||
id: authenticationProviders[0].id,
|
||||
@@ -119,7 +117,6 @@ describe("#authenticationProviders.update", () => {
|
||||
it("should require authentication", async () => {
|
||||
const team = await buildTeam();
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
|
||||
const res = await server.post("/api/authenticationProviders.update", {
|
||||
body: {
|
||||
id: authenticationProviders[0].id,
|
||||
@@ -129,17 +126,18 @@ describe("#authenticationProviders.update", () => {
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#authenticationProviders.list", () => {
|
||||
it("should return enabled and available auth providers", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/api/authenticationProviders.list", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.authenticationProviders.length).toBe(2);
|
||||
expect(body.data.authenticationProviders[0].name).toBe("slack");
|
||||
@@ -1,12 +1,13 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { AuthenticationProvider, Event } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { AuthenticationProvider, Event } from "@server/models";
|
||||
import policy from "@server/policies";
|
||||
import {
|
||||
presentAuthenticationProvider,
|
||||
presentPolicies,
|
||||
} from "../../presenters";
|
||||
} 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();
|
||||
@@ -14,12 +15,10 @@ const { authorize } = policy;
|
||||
|
||||
router.post("authenticationProviders.info", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
|
||||
assertUuid(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const authenticationProvider = await AuthenticationProvider.findByPk(id);
|
||||
authorize(user, "read", authenticationProvider);
|
||||
|
||||
ctx.body = {
|
||||
data: presentAuthenticationProvider(authenticationProvider),
|
||||
policies: presentPolicies(user, [authenticationProvider]),
|
||||
@@ -28,14 +27,13 @@ router.post("authenticationProviders.info", auth(), async (ctx) => {
|
||||
|
||||
router.post("authenticationProviders.update", auth(), async (ctx) => {
|
||||
const { id, isEnabled } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
ctx.assertPresent(isEnabled, "isEnabled is required");
|
||||
|
||||
assertUuid(id, "id is required");
|
||||
assertPresent(isEnabled, "isEnabled is required");
|
||||
const user = ctx.state.user;
|
||||
const authenticationProvider = await AuthenticationProvider.findByPk(id);
|
||||
authorize(user, "update", authenticationProvider);
|
||||
|
||||
const enabled = !!isEnabled;
|
||||
|
||||
if (enabled) {
|
||||
await authenticationProvider.enable();
|
||||
} else {
|
||||
@@ -44,13 +42,14 @@ router.post("authenticationProviders.update", auth(), async (ctx) => {
|
||||
|
||||
await Event.create({
|
||||
name: "authenticationProviders.update",
|
||||
data: { enabled },
|
||||
data: {
|
||||
enabled,
|
||||
},
|
||||
modelId: id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentAuthenticationProvider(authenticationProvider),
|
||||
policies: presentPolicies(user, [authenticationProvider]),
|
||||
@@ -60,17 +59,16 @@ router.post("authenticationProviders.update", auth(), async (ctx) => {
|
||||
router.post("authenticationProviders.list", auth(), async (ctx) => {
|
||||
const user = ctx.state.user;
|
||||
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 otherAuthenticationProviders = allAuthenticationProviders.filter(
|
||||
(p) =>
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 't' implicitly has an 'any' type.
|
||||
!teamAuthenticationProviders.find((t) => t.name === p.id) &&
|
||||
p.enabled &&
|
||||
// email auth is dealt with separetly right now, although it definitely
|
||||
p.enabled && // email auth is dealt with separetly right now, although it definitely
|
||||
// wants to be here in the future – we'll need to migrate more data though
|
||||
p.id !== "email"
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
authenticationProviders: [
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,8 @@
|
||||
// @flow
|
||||
import fractionalIndex from "fractional-index";
|
||||
import Router from "koa-router";
|
||||
import collectionExporter from "../../commands/collectionExporter";
|
||||
import { ValidationError } from "../../errors";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import collectionExporter from "@server/commands/collectionExporter";
|
||||
import { ValidationError } from "@server/errors";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import {
|
||||
Collection,
|
||||
CollectionUser,
|
||||
@@ -13,8 +12,8 @@ import {
|
||||
User,
|
||||
Group,
|
||||
Attachment,
|
||||
} from "../../models";
|
||||
import policy from "../../policies";
|
||||
} from "@server/models";
|
||||
import policy from "@server/policies";
|
||||
import {
|
||||
presentCollection,
|
||||
presentUser,
|
||||
@@ -23,11 +22,17 @@ import {
|
||||
presentGroup,
|
||||
presentCollectionGroupMembership,
|
||||
presentFileOperation,
|
||||
} from "../../presenters";
|
||||
import { Op, sequelize } from "../../sequelize";
|
||||
|
||||
import collectionIndexing from "../../utils/collectionIndexing";
|
||||
import removeIndexCollision from "../../utils/removeIndexCollision";
|
||||
} from "@server/presenters";
|
||||
import { Op, sequelize } from "@server/sequelize";
|
||||
import collectionIndexing from "@server/utils/collectionIndexing";
|
||||
import removeIndexCollision from "@server/utils/removeIndexCollision";
|
||||
import {
|
||||
assertUuid,
|
||||
assertIn,
|
||||
assertPresent,
|
||||
assertHexColor,
|
||||
assertIndexCharacters,
|
||||
} from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const { authorize } = policy;
|
||||
@@ -43,19 +48,20 @@ router.post("collections.create", auth(), async (ctx) => {
|
||||
icon,
|
||||
sort = Collection.DEFAULT_SORT,
|
||||
} = ctx.body;
|
||||
|
||||
let { index } = ctx.body;
|
||||
ctx.assertPresent(name, "name is required");
|
||||
assertPresent(name, "name is required");
|
||||
|
||||
if (color) {
|
||||
ctx.assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
|
||||
assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
|
||||
}
|
||||
|
||||
const user = ctx.state.user;
|
||||
authorize(user, "createCollection", user.team);
|
||||
|
||||
const collections = await Collection.findAll({
|
||||
where: { teamId: user.teamId, deletedAt: null },
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
deletedAt: null,
|
||||
},
|
||||
attributes: ["id", "index", "updatedAt"],
|
||||
limit: 1,
|
||||
order: [
|
||||
@@ -66,7 +72,7 @@ router.post("collections.create", auth(), async (ctx) => {
|
||||
});
|
||||
|
||||
if (index) {
|
||||
ctx.assertIndexCharacters(
|
||||
assertIndexCharacters(
|
||||
index,
|
||||
"Index characters must be between x20 to x7E ASCII"
|
||||
);
|
||||
@@ -78,7 +84,6 @@ router.post("collections.create", auth(), async (ctx) => {
|
||||
}
|
||||
|
||||
index = await removeIndexCollision(user.teamId, index);
|
||||
|
||||
let collection = await Collection.create({
|
||||
name,
|
||||
description,
|
||||
@@ -91,21 +96,20 @@ router.post("collections.create", auth(), async (ctx) => {
|
||||
sort,
|
||||
index,
|
||||
});
|
||||
|
||||
await Event.create({
|
||||
name: "collections.create",
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: user.id,
|
||||
data: { name },
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
// we must reload the collection to get memberships for policy presenter
|
||||
collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collection.id);
|
||||
|
||||
ctx.body = {
|
||||
data: presentCollection(collection),
|
||||
policies: presentPolicies(user, [collection]),
|
||||
@@ -114,14 +118,12 @@ router.post("collections.create", auth(), async (ctx) => {
|
||||
|
||||
router.post("collections.info", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, "id is required");
|
||||
|
||||
assertPresent(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
authorize(user, "read", collection);
|
||||
|
||||
ctx.body = {
|
||||
data: presentCollection(collection),
|
||||
policies: presentPolicies(user, [collection]),
|
||||
@@ -130,24 +132,22 @@ router.post("collections.info", auth(), async (ctx) => {
|
||||
|
||||
router.post("collections.import", auth(), async (ctx) => {
|
||||
const { type, attachmentId } = ctx.body;
|
||||
ctx.assertIn(type, ["outline"], "type must be one of 'outline'");
|
||||
ctx.assertUuid(attachmentId, "attachmentId is required");
|
||||
|
||||
assertIn(type, ["outline"], "type must be one of 'outline'");
|
||||
assertUuid(attachmentId, "attachmentId is required");
|
||||
const user = ctx.state.user;
|
||||
authorize(user, "importCollection", user.team);
|
||||
|
||||
const attachment = await Attachment.findByPk(attachmentId);
|
||||
authorize(user, "read", attachment);
|
||||
|
||||
await Event.create({
|
||||
name: "collections.import",
|
||||
modelId: attachmentId,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: { type },
|
||||
data: {
|
||||
type,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -155,17 +155,14 @@ router.post("collections.import", auth(), async (ctx) => {
|
||||
|
||||
router.post("collections.add_group", auth(), async (ctx) => {
|
||||
const { id, groupId, permission = "read_write" } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
ctx.assertUuid(groupId, "groupId is required");
|
||||
|
||||
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,
|
||||
@@ -190,10 +187,12 @@ router.post("collections.add_group", auth(), async (ctx) => {
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: ctx.state.user.id,
|
||||
data: { name: group.name, groupId },
|
||||
data: {
|
||||
name: group.name,
|
||||
groupId,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
collectionGroupMemberships: [
|
||||
@@ -205,28 +204,26 @@ router.post("collections.add_group", auth(), async (ctx) => {
|
||||
|
||||
router.post("collections.remove_group", auth(), async (ctx) => {
|
||||
const { id, groupId } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
ctx.assertUuid(groupId, "groupId is required");
|
||||
|
||||
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 Event.create({
|
||||
name: "collections.remove_group",
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: ctx.state.user.id,
|
||||
data: { name: group.name, groupId },
|
||||
data: {
|
||||
name: group.name,
|
||||
groupId,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -238,19 +235,15 @@ router.post(
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
const { id, query, permission } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
|
||||
assertUuid(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
|
||||
authorize(user, "read", collection);
|
||||
|
||||
let where = {
|
||||
collectionId: id,
|
||||
};
|
||||
|
||||
let groupWhere;
|
||||
|
||||
if (query) {
|
||||
@@ -262,10 +255,8 @@ router.post(
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
where = {
|
||||
...where,
|
||||
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 };
|
||||
}
|
||||
|
||||
const memberships = await CollectionGroup.findAll({
|
||||
@@ -282,13 +273,13 @@ router.post(
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: {
|
||||
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)),
|
||||
},
|
||||
};
|
||||
@@ -297,17 +288,14 @@ router.post(
|
||||
|
||||
router.post("collections.add_user", auth(), async (ctx) => {
|
||||
const { id, userId, permission = "read_write" } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
ctx.assertUuid(userId, "userId is required");
|
||||
|
||||
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,
|
||||
@@ -333,10 +321,11 @@ router.post("collections.add_user", auth(), async (ctx) => {
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: ctx.state.user.id,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
users: [presentUser(user)],
|
||||
@@ -347,47 +336,40 @@ router.post("collections.add_user", auth(), async (ctx) => {
|
||||
|
||||
router.post("collections.remove_user", auth(), async (ctx) => {
|
||||
const { id, userId } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
ctx.assertUuid(userId, "userId is required");
|
||||
|
||||
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 Event.create({
|
||||
name: "collections.remove_user",
|
||||
userId,
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: ctx.state.user.id,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
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;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
|
||||
assertUuid(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
authorize(user, "read", collection);
|
||||
|
||||
const users = await collection.getUsers();
|
||||
|
||||
ctx.body = {
|
||||
data: users.map(presentUser),
|
||||
};
|
||||
@@ -395,18 +377,15 @@ router.post("collections.users", auth(), async (ctx) => {
|
||||
|
||||
router.post("collections.memberships", auth(), pagination(), async (ctx) => {
|
||||
const { id, query, permission } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
|
||||
assertUuid(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
authorize(user, "read", collection);
|
||||
|
||||
let where = {
|
||||
collectionId: id,
|
||||
};
|
||||
|
||||
let userWhere;
|
||||
|
||||
if (query) {
|
||||
@@ -418,10 +397,8 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => {
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
where = {
|
||||
...where,
|
||||
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 };
|
||||
}
|
||||
|
||||
const memberships = await CollectionUser.findAll({
|
||||
@@ -438,11 +415,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)),
|
||||
},
|
||||
};
|
||||
@@ -450,29 +427,26 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => {
|
||||
|
||||
router.post("collections.export", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
|
||||
assertUuid(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
authorize(user, "export", team);
|
||||
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
|
||||
ctx.assertPresent(collection, "Collection should be present");
|
||||
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: { fileOperation: presentFileOperation(fileOperation) },
|
||||
data: {
|
||||
fileOperation: presentFileOperation(fileOperation),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -480,21 +454,21 @@ router.post("collections.export_all", auth(), async (ctx) => {
|
||||
const user = ctx.state.user;
|
||||
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: { fileOperation: presentFileOperation(fileOperation) },
|
||||
data: {
|
||||
fileOperation: presentFileOperation(fileOperation),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
router.post("collections.update", auth(), async (ctx) => {
|
||||
let {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
@@ -506,14 +480,13 @@ router.post("collections.update", auth(), async (ctx) => {
|
||||
} = ctx.body;
|
||||
|
||||
if (color) {
|
||||
ctx.assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
|
||||
assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
|
||||
}
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
|
||||
authorize(user, "update", collection);
|
||||
|
||||
// we're making this collection have no default access, ensure that the current
|
||||
@@ -537,18 +510,22 @@ router.post("collections.update", auth(), async (ctx) => {
|
||||
if (name !== undefined) {
|
||||
collection.name = name;
|
||||
}
|
||||
|
||||
if (description !== undefined) {
|
||||
collection.description = description;
|
||||
}
|
||||
|
||||
if (icon !== undefined) {
|
||||
collection.icon = icon;
|
||||
}
|
||||
|
||||
if (color !== undefined) {
|
||||
collection.color = color;
|
||||
}
|
||||
|
||||
if (permission !== undefined) {
|
||||
// frontend sends empty string
|
||||
ctx.assertIn(
|
||||
assertIn(
|
||||
permission,
|
||||
["read_write", "read", "", null],
|
||||
"Invalid permission"
|
||||
@@ -556,22 +533,25 @@ router.post("collections.update", auth(), async (ctx) => {
|
||||
privacyChanged = permission !== collection.permission;
|
||||
collection.permission = permission ? permission : null;
|
||||
}
|
||||
|
||||
if (sharing !== undefined) {
|
||||
sharingChanged = sharing !== collection.sharing;
|
||||
collection.sharing = sharing;
|
||||
}
|
||||
|
||||
if (sort !== undefined) {
|
||||
collection.sort = sort;
|
||||
}
|
||||
|
||||
await collection.save();
|
||||
|
||||
await Event.create({
|
||||
name: "collections.update",
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: user.id,
|
||||
data: { name },
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
@@ -581,7 +561,10 @@ router.post("collections.update", auth(), async (ctx) => {
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: user.id,
|
||||
data: { privacyChanged, sharingChanged },
|
||||
data: {
|
||||
privacyChanged,
|
||||
sharingChanged,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
}
|
||||
@@ -601,7 +584,7 @@ router.post("collections.update", auth(), async (ctx) => {
|
||||
router.post("collections.list", auth(), pagination(), async (ctx) => {
|
||||
const user = ctx.state.user;
|
||||
const collectionIds = await user.collectionIds();
|
||||
let collections = await Collection.scope({
|
||||
const collections = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findAll({
|
||||
where: {
|
||||
@@ -612,13 +595,14 @@ router.post("collections.list", auth(), pagination(), async (ctx) => {
|
||||
offset: ctx.state.pagination.offset,
|
||||
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];
|
||||
});
|
||||
@@ -634,28 +618,27 @@ router.post("collections.list", auth(), pagination(), async (ctx) => {
|
||||
router.post("collections.delete", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
const user = ctx.state.user;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(id);
|
||||
|
||||
authorize(user, "delete", collection);
|
||||
|
||||
const total = await Collection.count();
|
||||
if (total === 1) throw new ValidationError("Cannot delete last collection");
|
||||
if (total === 1) throw ValidationError("Cannot delete last collection");
|
||||
|
||||
await collection.destroy();
|
||||
|
||||
await Event.create({
|
||||
name: "collections.delete",
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: user.id,
|
||||
data: { name: collection.name },
|
||||
data: {
|
||||
name: collection.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -664,35 +647,35 @@ router.post("collections.delete", auth(), async (ctx) => {
|
||||
router.post("collections.move", auth(), async (ctx) => {
|
||||
const id = ctx.body.id;
|
||||
let index = ctx.body.index;
|
||||
|
||||
ctx.assertPresent(index, "index is required");
|
||||
ctx.assertIndexCharacters(
|
||||
assertPresent(index, "index is required");
|
||||
assertIndexCharacters(
|
||||
index,
|
||||
"Index characters must be between x20 to x7E ASCII"
|
||||
);
|
||||
ctx.assertUuid(id, "id must be a uuid");
|
||||
|
||||
assertUuid(id, "id must be a uuid");
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.findByPk(id);
|
||||
|
||||
authorize(user, "move", collection);
|
||||
|
||||
index = await removeIndexCollision(user.teamId, index);
|
||||
|
||||
await collection.update({ index });
|
||||
|
||||
await collection.update({
|
||||
index,
|
||||
});
|
||||
await Event.create({
|
||||
name: "collections.move",
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: user.id,
|
||||
data: { index },
|
||||
data: {
|
||||
index,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
data: { index },
|
||||
data: {
|
||||
index,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,16 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 webService from "../../services/web";
|
||||
import { buildEvent, buildUser } from "../../test/factories";
|
||||
import { flushdb, seed } from "../../test/support";
|
||||
import webService from "@server/services/web";
|
||||
import { buildEvent, buildUser } from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
|
||||
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();
|
||||
|
||||
// audit event
|
||||
await buildEvent({
|
||||
name: "users.promote",
|
||||
@@ -20,7 +18,6 @@ describe("#events.list", () => {
|
||||
actorId: admin.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// event viewable in activity stream
|
||||
const event = await buildEvent({
|
||||
name: "documents.publish",
|
||||
@@ -29,12 +26,12 @@ describe("#events.list", () => {
|
||||
teamId: user.teamId,
|
||||
actorId: admin.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/events.list", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(event.id);
|
||||
@@ -42,7 +39,6 @@ describe("#events.list", () => {
|
||||
|
||||
it("should return audit events", async () => {
|
||||
const { user, admin, document, collection } = await seed();
|
||||
|
||||
// audit event
|
||||
const auditEvent = await buildEvent({
|
||||
name: "users.promote",
|
||||
@@ -50,7 +46,6 @@ describe("#events.list", () => {
|
||||
actorId: admin.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// event viewable in activity stream
|
||||
const event = await buildEvent({
|
||||
name: "documents.publish",
|
||||
@@ -59,12 +54,13 @@ describe("#events.list", () => {
|
||||
teamId: user.teamId,
|
||||
actorId: admin.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/events.list", {
|
||||
body: { token: admin.getJwtToken(), auditLog: true },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
auditLog: true,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(2);
|
||||
expect(body.data[0].id).toEqual(event.id);
|
||||
@@ -73,7 +69,6 @@ describe("#events.list", () => {
|
||||
|
||||
it("should allow filtering by actorId", async () => {
|
||||
const { user, admin, document, collection } = await seed();
|
||||
|
||||
// audit event
|
||||
const auditEvent = await buildEvent({
|
||||
name: "users.promote",
|
||||
@@ -81,7 +76,6 @@ describe("#events.list", () => {
|
||||
actorId: admin.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// event viewable in activity stream
|
||||
await buildEvent({
|
||||
name: "documents.publish",
|
||||
@@ -90,12 +84,14 @@ describe("#events.list", () => {
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/events.list", {
|
||||
body: { token: admin.getJwtToken(), auditLog: true, actorId: admin.id },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
auditLog: true,
|
||||
actorId: admin.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(auditEvent.id);
|
||||
@@ -103,7 +99,6 @@ describe("#events.list", () => {
|
||||
|
||||
it("should allow filtering by documentId", async () => {
|
||||
const { user, admin, document, collection } = await seed();
|
||||
|
||||
const event = await buildEvent({
|
||||
name: "documents.publish",
|
||||
collectionId: collection.id,
|
||||
@@ -111,7 +106,6 @@ describe("#events.list", () => {
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/events.list", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
@@ -119,7 +113,6 @@ describe("#events.list", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(event.id);
|
||||
@@ -128,7 +121,6 @@ describe("#events.list", () => {
|
||||
it("should not return events for documentId without authorization", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
const actor = await buildUser();
|
||||
|
||||
await buildEvent({
|
||||
name: "documents.publish",
|
||||
collectionId: collection.id,
|
||||
@@ -136,7 +128,6 @@ describe("#events.list", () => {
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/events.list", {
|
||||
body: {
|
||||
token: actor.getJwtToken(),
|
||||
@@ -144,14 +135,12 @@ describe("#events.list", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should allow filtering by event name", async () => {
|
||||
const { user, admin, document, collection } = await seed();
|
||||
|
||||
// audit event
|
||||
await buildEvent({
|
||||
name: "users.promote",
|
||||
@@ -159,7 +148,6 @@ describe("#events.list", () => {
|
||||
actorId: admin.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// event viewable in activity stream
|
||||
const event = await buildEvent({
|
||||
name: "documents.publish",
|
||||
@@ -168,7 +156,6 @@ describe("#events.list", () => {
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/events.list", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -176,7 +163,6 @@ describe("#events.list", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(event.id);
|
||||
@@ -184,7 +170,6 @@ describe("#events.list", () => {
|
||||
|
||||
it("should return events with deleted actors", async () => {
|
||||
const { user, admin, document, collection } = await seed();
|
||||
|
||||
// event viewable in activity stream
|
||||
const event = await buildEvent({
|
||||
name: "documents.publish",
|
||||
@@ -193,15 +178,13 @@ describe("#events.list", () => {
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
});
|
||||
|
||||
await user.destroy();
|
||||
|
||||
const res = await server.post("/api/events.list", {
|
||||
body: { token: admin.getJwtToken() },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(event.id);
|
||||
@@ -210,16 +193,17 @@ describe("#events.list", () => {
|
||||
it("should require authorization for audit events", async () => {
|
||||
const { user } = await seed();
|
||||
const res = await server.post("/api/events.list", {
|
||||
body: { token: user.getJwtToken(), auditLog: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
auditLog: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/events.list");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import Sequelize from "sequelize";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Event, User, Collection } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { presentEvent } from "../../presenters";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Event, User, Collection } from "@server/models";
|
||||
import policy from "@server/policies";
|
||||
import { presentEvent } from "@server/presenters";
|
||||
import { assertSort, assertUuid } from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const Op = Sequelize.Op;
|
||||
@@ -13,47 +13,53 @@ const router = new Router();
|
||||
|
||||
router.post("events.list", auth(), pagination(), async (ctx) => {
|
||||
const user = ctx.state.user;
|
||||
let {
|
||||
let { direction } = ctx.body;
|
||||
const {
|
||||
sort = "createdAt",
|
||||
actorId,
|
||||
documentId,
|
||||
collectionId,
|
||||
direction,
|
||||
name,
|
||||
auditLog = false,
|
||||
} = ctx.body;
|
||||
if (direction !== "ASC") direction = "DESC";
|
||||
ctx.assertSort(sort, Event);
|
||||
|
||||
assertSort(sort, Event);
|
||||
let where = {
|
||||
name: Event.ACTIVITY_EVENTS,
|
||||
teamId: user.teamId,
|
||||
};
|
||||
|
||||
if (actorId) {
|
||||
ctx.assertUuid(actorId, "actorId must be a UUID");
|
||||
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) {
|
||||
ctx.assertUuid(documentId, "documentId must be a UUID");
|
||||
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 (collectionId) {
|
||||
ctx.assertUuid(collectionId, "collection must be a UUID");
|
||||
|
||||
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);
|
||||
authorize(user, "read", collection);
|
||||
} else {
|
||||
const collectionIds = await user.collectionIds({ paranoid: false });
|
||||
const collectionIds = await user.collectionIds({
|
||||
paranoid: false,
|
||||
});
|
||||
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: collectionIds,
|
||||
},
|
||||
{
|
||||
collectionId: {
|
||||
[Op.eq]: null,
|
||||
@@ -85,9 +91,9 @@ 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)),
|
||||
};
|
||||
});
|
||||
@@ -1,41 +1,41 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 { Collection, User, Event, FileOperation } from "../../models";
|
||||
import webService from "../../services/web";
|
||||
import { Collection, User, Event, FileOperation } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
import {
|
||||
buildAdmin,
|
||||
buildCollection,
|
||||
buildFileOperation,
|
||||
buildTeam,
|
||||
buildUser,
|
||||
} from "../../test/factories";
|
||||
import { flushdb } from "../../test/support";
|
||||
} 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 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
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();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/fileOperations.info", {
|
||||
body: {
|
||||
id: exportData.id,
|
||||
@@ -43,9 +43,7 @@ describe("#fileOperations.info", () => {
|
||||
type: "export",
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(exportData.id);
|
||||
expect(body.data.state).toBe(exportData.state);
|
||||
@@ -53,14 +51,17 @@ describe("#fileOperations.info", () => {
|
||||
|
||||
it("should require user to be an admin", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/fileOperations.info", {
|
||||
body: {
|
||||
id: exportData.id,
|
||||
@@ -68,31 +69,28 @@ describe("#fileOperations.info", () => {
|
||||
type: "export",
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fileOperations.list", () => {
|
||||
it("should return fileOperations list", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/fileOperations.list", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
type: "export",
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
const data = body.data[0];
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toBe(1);
|
||||
expect(data.id).toBe(exportData.id);
|
||||
@@ -102,29 +100,27 @@ describe("#fileOperations.list", () => {
|
||||
|
||||
it("should return exports with collection data", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
userId: admin.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/fileOperations.list", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
type: "export",
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
const data = body.data[0];
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toBe(1);
|
||||
expect(data.id).toBe(exportData.id);
|
||||
@@ -135,35 +131,30 @@ describe("#fileOperations.list", () => {
|
||||
|
||||
it("should return exports with collection data even if collection is deleted", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
userId: admin.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
|
||||
await collection.destroy();
|
||||
|
||||
const isCollectionPresent = await Collection.findByPk(collection.id);
|
||||
|
||||
expect(isCollectionPresent).toBe(null);
|
||||
|
||||
const res = await server.post("/api/fileOperations.list", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
type: "export",
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
const data = body.data[0];
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toBe(1);
|
||||
expect(data.id).toBe(exportData.id);
|
||||
@@ -174,36 +165,33 @@ describe("#fileOperations.list", () => {
|
||||
|
||||
it("should return exports with user data even if user is deleted", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin2 = await buildAdmin({ teamId: team.id });
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const admin2 = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const collection = await buildCollection({
|
||||
userId: admin.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
|
||||
await admin.destroy();
|
||||
|
||||
const isAdminPresent = await User.findByPk(admin.id);
|
||||
|
||||
expect(isAdminPresent).toBe(null);
|
||||
|
||||
const res = await server.post("/api/fileOperations.list", {
|
||||
body: {
|
||||
token: admin2.getJwtToken(),
|
||||
type: "export",
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
const data = body.data[0];
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toBe(1);
|
||||
expect(data.id).toBe(exportData.id);
|
||||
@@ -214,55 +202,54 @@ describe("#fileOperations.list", () => {
|
||||
|
||||
it("should require authorization", async () => {
|
||||
const user = await buildUser();
|
||||
|
||||
const res = await server.post("/api/fileOperations.list", {
|
||||
body: { token: user.getJwtToken(), type: "export" },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
type: "export",
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fileOperations.redirect", () => {
|
||||
it("should not redirect when file operation is not complete", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/fileOperations.redirect", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: exportData.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.message).toEqual("export is not complete yet");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fileOperations.info", () => {
|
||||
it("should return file operation", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/fileOperations.info", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: exportData.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
expect(res.status).toBe(200);
|
||||
expect(body.data.id).toBe(exportData.id);
|
||||
@@ -271,43 +258,44 @@ describe("#fileOperations.info", () => {
|
||||
|
||||
it("should require authorization", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/fileOperations.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: exportData.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fileOperations.delete", () => {
|
||||
it("should delete file operation", async () => {
|
||||
const team = await buildTeam();
|
||||
const admin = await buildAdmin({ teamId: team.id });
|
||||
const admin = await buildAdmin({
|
||||
teamId: team.id,
|
||||
});
|
||||
const exportData = await buildFileOperation({
|
||||
type: "export",
|
||||
teamId: team.id,
|
||||
userId: admin.id,
|
||||
state: "complete",
|
||||
});
|
||||
|
||||
const deleteResponse = await server.post("/api/fileOperations.delete", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: exportData.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(deleteResponse.status).toBe(200);
|
||||
expect(await Event.count()).toBe(1);
|
||||
expect(await FileOperation.count()).toBe(0);
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import fileOperationDeleter from "../../commands/fileOperationDeleter";
|
||||
import { NotFoundError, ValidationError } from "../../errors";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { FileOperation, Team } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { presentFileOperation } from "../../presenters";
|
||||
import { getSignedUrl } from "../../utils/s3";
|
||||
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 { 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;
|
||||
@@ -14,16 +14,14 @@ const router = new Router();
|
||||
|
||||
router.post("fileOperations.info", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
|
||||
const fileOperation = await FileOperation.findByPk(id);
|
||||
|
||||
authorize(user, fileOperation.type, team);
|
||||
|
||||
if (!fileOperation) {
|
||||
throw new NotFoundError();
|
||||
throw NotFoundError();
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
@@ -32,27 +30,23 @@ router.post("fileOperations.info", auth(), async (ctx) => {
|
||||
});
|
||||
|
||||
router.post("fileOperations.list", auth(), pagination(), async (ctx) => {
|
||||
let { sort = "createdAt", direction, type } = ctx.body;
|
||||
|
||||
ctx.assertPresent(type, "type is required");
|
||||
ctx.assertIn(
|
||||
let { direction } = ctx.body;
|
||||
const { sort = "createdAt", type } = ctx.body;
|
||||
assertPresent(type, "type is required");
|
||||
assertIn(
|
||||
type,
|
||||
["import", "export"],
|
||||
"type must be one of 'import' or 'export'"
|
||||
);
|
||||
|
||||
if (direction !== "ASC") direction = "DESC";
|
||||
|
||||
const user = ctx.state.user;
|
||||
|
||||
const where = {
|
||||
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,
|
||||
@@ -64,57 +58,51 @@ router.post("fileOperations.list", auth(), pagination(), async (ctx) => {
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: {
|
||||
...ctx.state.pagination,
|
||||
total,
|
||||
},
|
||||
pagination: { ...ctx.state.pagination, total },
|
||||
data: exports.map(presentFileOperation),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("fileOperations.redirect", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
const fileOp = await FileOperation.unscoped().findByPk(id);
|
||||
|
||||
if (!fileOp) {
|
||||
throw new NotFoundError();
|
||||
throw NotFoundError();
|
||||
}
|
||||
|
||||
authorize(user, fileOp.type, team);
|
||||
|
||||
if (fileOp.state !== "complete") {
|
||||
throw new ValidationError(`${fileOp.type} is not complete yet`);
|
||||
throw ValidationError(`${fileOp.type} is not complete yet`);
|
||||
}
|
||||
|
||||
const accessUrl = await getSignedUrl(fileOp.key);
|
||||
|
||||
ctx.redirect(accessUrl);
|
||||
});
|
||||
|
||||
router.post("fileOperations.delete", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
const fileOp = await FileOperation.findByPk(id);
|
||||
|
||||
if (!fileOp) {
|
||||
throw new NotFoundError();
|
||||
throw NotFoundError();
|
||||
}
|
||||
|
||||
authorize(user, fileOp.type, team);
|
||||
|
||||
await fileOperationDeleter(fileOp, user, ctx.request.ip);
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,39 +1,39 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 { Event } from "../../models";
|
||||
import webService from "../../services/web";
|
||||
import { buildUser, buildAdmin, buildGroup } from "../../test/factories";
|
||||
import { flushdb } from "../../test/support";
|
||||
import { Event } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildAdmin, buildGroup } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
|
||||
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";
|
||||
const user = await buildAdmin();
|
||||
|
||||
const res = await server.post("/api/groups.create", {
|
||||
body: { token: user.getJwtToken(), name },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toEqual(name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#groups.update", () => {
|
||||
it("should require authentication", async () => {
|
||||
const group = await buildGroup();
|
||||
const res = await server.post("/api/groups.update", {
|
||||
body: { id: group.id, name: "Test" },
|
||||
body: {
|
||||
id: group.id,
|
||||
name: "Test",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -42,7 +42,11 @@ describe("#groups.update", () => {
|
||||
const group = await buildGroup();
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/groups.update", {
|
||||
body: { token: user.getJwtToken(), id: group.id, name: "Test" },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
name: "Test",
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
@@ -50,165 +54,196 @@ describe("#groups.update", () => {
|
||||
it("should require authorization", async () => {
|
||||
const group = await buildGroup();
|
||||
const user = await buildAdmin();
|
||||
|
||||
const res = await server.post("/api/groups.update", {
|
||||
body: { token: user.getJwtToken(), id: group.id, name: "Test" },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
name: "Test",
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
describe("when user is admin", () => {
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'user' implicitly has type 'any' in some ... Remove this comment to see the full error message
|
||||
let user, group;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await buildAdmin();
|
||||
group = await buildGroup({ teamId: user.teamId });
|
||||
group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
});
|
||||
|
||||
it("allows admin to edit a group", async () => {
|
||||
const res = await server.post("/api/groups.update", {
|
||||
body: { token: user.getJwtToken(), id: group.id, name: "Test" },
|
||||
body: {
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'user' implicitly has an 'any' type.
|
||||
token: user.getJwtToken(),
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'group' implicitly has an 'any' type.
|
||||
id: group.id,
|
||||
name: "Test",
|
||||
},
|
||||
});
|
||||
|
||||
const events = await Event.findAll();
|
||||
expect(events.length).toEqual(1);
|
||||
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toBe("Test");
|
||||
});
|
||||
|
||||
it("does not create an event if the update is a noop", async () => {
|
||||
const res = await server.post("/api/groups.update", {
|
||||
body: { token: user.getJwtToken(), id: group.id, name: group.name },
|
||||
body: {
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'user' implicitly has an 'any' type.
|
||||
token: user.getJwtToken(),
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'group' implicitly has an 'any' type.
|
||||
id: group.id,
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'group' implicitly has an 'any' type.
|
||||
name: group.name,
|
||||
},
|
||||
});
|
||||
|
||||
const events = await Event.findAll();
|
||||
expect(events.length).toEqual(0);
|
||||
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'group' implicitly has an 'any' type.
|
||||
expect(body.data.name).toBe(group.name);
|
||||
});
|
||||
|
||||
it("fails with validation error when name already taken", async () => {
|
||||
await buildGroup({
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'user' implicitly has an 'any' type.
|
||||
teamId: user.teamId,
|
||||
name: "test",
|
||||
});
|
||||
|
||||
const res = await server.post("/api/groups.update", {
|
||||
body: {
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'user' implicitly has an 'any' type.
|
||||
token: user.getJwtToken(),
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'group' implicitly has an 'any' type.
|
||||
id: group.id,
|
||||
name: "TEST",
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#groups.list", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/groups.list");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should return groups with memberships preloaded", async () => {
|
||||
const user = await buildUser();
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
await group.addUser(user, { through: { createdById: user.id } });
|
||||
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await group.addUser(user, {
|
||||
through: {
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/groups.list", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
expect(body.data["groups"].length).toEqual(1);
|
||||
expect(body.data["groups"][0].id).toEqual(group.id);
|
||||
|
||||
expect(body.data["groupMemberships"].length).toEqual(1);
|
||||
expect(body.data["groupMemberships"][0].groupId).toEqual(group.id);
|
||||
expect(body.data["groupMemberships"][0].user.id).toEqual(user.id);
|
||||
|
||||
expect(body.policies.length).toEqual(1);
|
||||
expect(body.policies[0].abilities.read).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return groups when membership user is deleted", async () => {
|
||||
const me = await buildUser();
|
||||
const user = await buildUser({ teamId: me.teamId });
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
await group.addUser(user, { through: { createdById: me.id } });
|
||||
await group.addUser(me, { through: { createdById: me.id } });
|
||||
const user = await buildUser({
|
||||
teamId: me.teamId,
|
||||
});
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await group.addUser(user, {
|
||||
through: {
|
||||
createdById: me.id,
|
||||
},
|
||||
});
|
||||
await group.addUser(me, {
|
||||
through: {
|
||||
createdById: me.id,
|
||||
},
|
||||
});
|
||||
await user.destroy();
|
||||
|
||||
const res = await server.post("/api/groups.list", {
|
||||
body: { token: me.getJwtToken() },
|
||||
body: {
|
||||
token: me.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
expect(body.data["groups"].length).toEqual(1);
|
||||
expect(body.data["groups"][0].id).toEqual(group.id);
|
||||
|
||||
expect(body.data["groupMemberships"].length).toEqual(1);
|
||||
expect(body.data["groupMemberships"][0].groupId).toEqual(group.id);
|
||||
expect(body.data["groupMemberships"][0].user.id).toEqual(me.id);
|
||||
|
||||
expect(body.policies.length).toEqual(1);
|
||||
expect(body.policies[0].abilities.read).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#groups.info", () => {
|
||||
it("should return group if admin", async () => {
|
||||
const user = await buildAdmin();
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
const res = await server.post("/api/groups.info", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/groups.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toEqual(group.id);
|
||||
});
|
||||
|
||||
it("should return group if member", async () => {
|
||||
const user = await buildUser();
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
await group.addUser(user, { through: { createdById: user.id } });
|
||||
|
||||
const res = await server.post("/api/groups.info", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await group.addUser(user, {
|
||||
through: {
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/groups.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toEqual(group.id);
|
||||
});
|
||||
|
||||
it("should still return group if non-member, non-admin", async () => {
|
||||
const user = await buildUser();
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/groups.info", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
@@ -220,7 +255,6 @@ describe("#groups.info", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/groups.info");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -229,20 +263,23 @@ describe("#groups.info", () => {
|
||||
const user = await buildUser();
|
||||
const group = await buildGroup();
|
||||
const res = await server.post("/api/groups.info", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#groups.delete", () => {
|
||||
it("should require authentication", async () => {
|
||||
const group = await buildGroup();
|
||||
const res = await server.post("/api/groups.delete", {
|
||||
body: { id: group.id },
|
||||
body: {
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -251,7 +288,10 @@ describe("#groups.delete", () => {
|
||||
const group = await buildGroup();
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/groups.delete", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
@@ -259,40 +299,49 @@ describe("#groups.delete", () => {
|
||||
it("should require authorization", async () => {
|
||||
const group = await buildGroup();
|
||||
const user = await buildAdmin();
|
||||
|
||||
const res = await server.post("/api/groups.delete", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("allows admin to delete a group", async () => {
|
||||
const user = await buildAdmin();
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
const res = await server.post("/api/groups.delete", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/groups.delete", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
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, { through: { createdById: user.id } });
|
||||
|
||||
const res = await server.post("/api/groups.memberships", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await group.addUser(user, {
|
||||
through: {
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
const res = await server.post("/api/groups.memberships", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.users.length).toEqual(1);
|
||||
expect(body.data.users[0].id).toEqual(user.id);
|
||||
@@ -302,16 +351,32 @@ describe("#groups.memberships", () => {
|
||||
|
||||
it("should allow filtering members in group by name", async () => {
|
||||
const user = await buildUser();
|
||||
const user2 = await buildUser({ name: "Won't find" });
|
||||
const user3 = await buildUser({ teamId: user.teamId, name: "Deleted" });
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
await group.addUser(user, { through: { createdById: user.id } });
|
||||
await group.addUser(user2, { through: { createdById: user.id } });
|
||||
await group.addUser(user3, { through: { createdById: user.id } });
|
||||
|
||||
const user2 = await buildUser({
|
||||
name: "Won't find",
|
||||
});
|
||||
const user3 = await buildUser({
|
||||
teamId: user.teamId,
|
||||
name: "Deleted",
|
||||
});
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await group.addUser(user, {
|
||||
through: {
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
await group.addUser(user2, {
|
||||
through: {
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
await group.addUser(user3, {
|
||||
through: {
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
await user3.destroy();
|
||||
|
||||
const res = await server.post("/api/groups.memberships", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -320,7 +385,6 @@ describe("#groups.memberships", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.users.length).toEqual(1);
|
||||
expect(body.data.users[0].id).toEqual(user.id);
|
||||
@@ -329,7 +393,6 @@ describe("#groups.memberships", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/groups.memberships");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -337,21 +400,21 @@ describe("#groups.memberships", () => {
|
||||
it("should require authorization", async () => {
|
||||
const user = await buildUser();
|
||||
const group = await buildGroup();
|
||||
|
||||
const res = await server.post("/api/groups.memberships", {
|
||||
body: { token: user.getJwtToken(), id: group.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#groups.add_user", () => {
|
||||
it("should add user to group", async () => {
|
||||
const user = await buildAdmin();
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/groups.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -359,7 +422,6 @@ describe("#groups.add_user", () => {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const users = await group.getUsers();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(users.length).toEqual(1);
|
||||
@@ -376,7 +438,6 @@ describe("#groups.add_user", () => {
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const anotherUser = await buildUser();
|
||||
|
||||
const res = await server.post("/api/groups.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -384,88 +445,7 @@ describe("#groups.add_user", () => {
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should require admin", async () => {
|
||||
const user = await buildUser();
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const anotherUser = await buildUser({ teamId: user.teamId });
|
||||
|
||||
const res = await server.post("/api/groups.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#groups.remove_user", () => {
|
||||
it("should remove user from group", async () => {
|
||||
const user = await buildAdmin();
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
await server.post("/api/groups.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const users = await group.getUsers();
|
||||
expect(users.length).toEqual(1);
|
||||
|
||||
const res = await server.post("/api/groups.remove_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const users1 = await group.getUsers();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(users1.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/groups.remove_user");
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
|
||||
it("should require user in team", async () => {
|
||||
const user = await buildAdmin();
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const anotherUser = await buildUser();
|
||||
|
||||
const res = await server.post("/api/groups.remove_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -478,7 +458,56 @@ describe("#groups.remove_user", () => {
|
||||
const anotherUser = await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/groups.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe("#groups.remove_user", () => {
|
||||
it("should remove user from group", async () => {
|
||||
const user = await buildAdmin();
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await server.post("/api/groups.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
const users = await group.getUsers();
|
||||
expect(users.length).toEqual(1);
|
||||
const res = await server.post("/api/groups.remove_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
const users1 = await group.getUsers();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(users1.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/groups.remove_user");
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
|
||||
it("should require user in team", async () => {
|
||||
const user = await buildAdmin();
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const anotherUser = await buildUser();
|
||||
const res = await server.post("/api/groups.remove_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -487,7 +516,26 @@ describe("#groups.remove_user", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should require admin", async () => {
|
||||
const user = await buildUser();
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const anotherUser = await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/groups.remove_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: group.id,
|
||||
userId: anotherUser.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -1,30 +1,29 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import { MAX_AVATAR_DISPLAY } from "../../../shared/constants";
|
||||
import auth from "../../middlewares/authentication";
|
||||
|
||||
import { User, Event, Group, GroupUser } from "../../models";
|
||||
import policy from "../../policies";
|
||||
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 {
|
||||
presentGroup,
|
||||
presentPolicies,
|
||||
presentUser,
|
||||
presentGroupMembership,
|
||||
} from "../../presenters";
|
||||
import { Op } from "../../sequelize";
|
||||
} 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) => {
|
||||
let { sort = "updatedAt", direction } = ctx.body;
|
||||
let { direction } = ctx.body;
|
||||
const { sort = "updatedAt" } = ctx.body;
|
||||
if (direction !== "ASC") direction = "DESC";
|
||||
ctx.assertSort(sort, Group);
|
||||
|
||||
assertSort(sort, Group);
|
||||
const user = ctx.state.user;
|
||||
|
||||
let groups = await Group.findAll({
|
||||
const groups = await Group.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
},
|
||||
@@ -38,8 +37,10 @@ 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)
|
||||
)
|
||||
@@ -52,12 +53,11 @@ router.post("groups.list", auth(), pagination(), async (ctx) => {
|
||||
|
||||
router.post("groups.info", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const group = await Group.findByPk(id);
|
||||
authorize(user, "read", group);
|
||||
|
||||
ctx.body = {
|
||||
data: presentGroup(group),
|
||||
policies: presentPolicies(user, [group]),
|
||||
@@ -66,29 +66,27 @@ router.post("groups.info", auth(), async (ctx) => {
|
||||
|
||||
router.post("groups.create", auth(), async (ctx) => {
|
||||
const { name } = ctx.body;
|
||||
ctx.assertPresent(name, "name is required");
|
||||
assertPresent(name, "name is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
|
||||
authorize(user, "createGroup", user.team);
|
||||
let group = await Group.create({
|
||||
name,
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
});
|
||||
|
||||
// reload to get default scope
|
||||
group = await Group.findByPk(group.id);
|
||||
|
||||
await Event.create({
|
||||
name: "groups.create",
|
||||
actorId: user.id,
|
||||
teamId: user.teamId,
|
||||
modelId: group.id,
|
||||
data: { name: group.name },
|
||||
data: {
|
||||
name: group.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentGroup(group),
|
||||
policies: presentPolicies(user, [group]),
|
||||
@@ -97,14 +95,12 @@ router.post("groups.create", auth(), async (ctx) => {
|
||||
|
||||
router.post("groups.update", auth(), async (ctx) => {
|
||||
const { id, name } = ctx.body;
|
||||
ctx.assertPresent(name, "name is required");
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertPresent(name, "name is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const group = await Group.findByPk(id);
|
||||
|
||||
authorize(user, "update", group);
|
||||
|
||||
group.name = name;
|
||||
|
||||
if (group.changed()) {
|
||||
@@ -114,7 +110,9 @@ router.post("groups.update", auth(), async (ctx) => {
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
modelId: group.id,
|
||||
data: { name },
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
}
|
||||
@@ -127,23 +125,22 @@ router.post("groups.update", auth(), async (ctx) => {
|
||||
|
||||
router.post("groups.delete", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const { user } = ctx.state;
|
||||
const group = await Group.findByPk(id);
|
||||
|
||||
authorize(user, "delete", group);
|
||||
await group.destroy();
|
||||
|
||||
await Event.create({
|
||||
name: "groups.delete",
|
||||
actorId: user.id,
|
||||
modelId: group.id,
|
||||
teamId: group.teamId,
|
||||
data: { name: group.name },
|
||||
data: {
|
||||
name: group.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -151,13 +148,13 @@ router.post("groups.delete", auth(), async (ctx) => {
|
||||
|
||||
router.post("groups.memberships", auth(), pagination(), async (ctx) => {
|
||||
const { id, query } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const group = await Group.findByPk(id);
|
||||
authorize(user, "read", group);
|
||||
|
||||
let userWhere;
|
||||
|
||||
if (query) {
|
||||
userWhere = {
|
||||
name: {
|
||||
@@ -167,7 +164,9 @@ router.post("groups.memberships", auth(), pagination(), async (ctx) => {
|
||||
}
|
||||
|
||||
const memberships = await GroupUser.findAll({
|
||||
where: { groupId: id },
|
||||
where: {
|
||||
groupId: id,
|
||||
},
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
@@ -180,11 +179,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)),
|
||||
},
|
||||
};
|
||||
@@ -192,15 +191,13 @@ router.post("groups.memberships", auth(), pagination(), async (ctx) => {
|
||||
|
||||
router.post("groups.add_user", auth(), async (ctx) => {
|
||||
const { id, userId } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
ctx.assertUuid(userId, "userId is required");
|
||||
assertUuid(id, "id is required");
|
||||
assertUuid(userId, "userId is required");
|
||||
|
||||
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,
|
||||
@@ -210,9 +207,10 @@ router.post("groups.add_user", auth(), async (ctx) => {
|
||||
|
||||
if (!membership) {
|
||||
await group.addUser(user, {
|
||||
through: { createdById: ctx.state.user.id },
|
||||
through: {
|
||||
createdById: ctx.state.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
// reload to get default scope
|
||||
membership = await GroupUser.findOne({
|
||||
where: {
|
||||
@@ -220,17 +218,17 @@ router.post("groups.add_user", auth(), async (ctx) => {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
// reload to get default scope
|
||||
group = await Group.findByPk(id);
|
||||
|
||||
await Event.create({
|
||||
name: "groups.add_user",
|
||||
userId,
|
||||
teamId: user.teamId,
|
||||
modelId: group.id,
|
||||
actorId: ctx.state.user.id,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
}
|
||||
@@ -246,30 +244,27 @@ router.post("groups.add_user", auth(), async (ctx) => {
|
||||
|
||||
router.post("groups.remove_user", auth(), async (ctx) => {
|
||||
const { id, userId } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
ctx.assertUuid(userId, "userId is required");
|
||||
assertUuid(id, "id is required");
|
||||
assertUuid(userId, "userId is required");
|
||||
|
||||
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 Event.create({
|
||||
name: "groups.remove_user",
|
||||
userId,
|
||||
modelId: group.id,
|
||||
teamId: user.teamId,
|
||||
actorId: ctx.state.user.id,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
// reload to get default scope
|
||||
group = await Group.findByPk(id);
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
groups: [presentGroup(group)],
|
||||
@@ -1,21 +1,18 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 { IntegrationAuthentication, SearchQuery } from "../../models";
|
||||
import webService from "../../services/web";
|
||||
import { buildDocument, buildIntegration } from "../../test/factories";
|
||||
import { flushdb, seed } from "../../test/support";
|
||||
import * as Slack from "../../utils/slack";
|
||||
import { IntegrationAuthentication, SearchQuery } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
import { buildDocument, buildIntegration } from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
|
||||
const app = webService();
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
afterAll(() => server.close());
|
||||
|
||||
jest.mock("../../utils/slack", () => ({
|
||||
post: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("#hooks.unfurl", () => {
|
||||
it("should return documents", async () => {
|
||||
const { user, document } = await seed();
|
||||
@@ -25,7 +22,6 @@ describe("#hooks.unfurl", () => {
|
||||
teamId: user.teamId,
|
||||
token: "",
|
||||
});
|
||||
|
||||
const res = await server.post("/api/hooks.unfurl", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
@@ -49,11 +45,9 @@ describe("#hooks.unfurl", () => {
|
||||
expect(Slack.post).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#hooks.slack", () => {
|
||||
it("should return no matches", async () => {
|
||||
const { user, team } = await seed();
|
||||
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
@@ -132,7 +126,7 @@ 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) => {
|
||||
const { user, team } = await seed();
|
||||
await server.post("/api/hooks.slack", {
|
||||
@@ -143,12 +137,13 @@ 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" },
|
||||
where: {
|
||||
query: "contains",
|
||||
},
|
||||
});
|
||||
expect(searchQuery.length).toBe(1);
|
||||
expect(searchQuery[0].results).toBe(0);
|
||||
@@ -189,7 +184,6 @@ describe("#hooks.slack", () => {
|
||||
|
||||
it("should return search results with snippet for unknown user", async () => {
|
||||
const { user, team } = await seed();
|
||||
|
||||
// unpublished document will not be returned
|
||||
await buildDocument({
|
||||
text: "This title contains a search term",
|
||||
@@ -197,7 +191,6 @@ describe("#hooks.slack", () => {
|
||||
teamId: user.teamId,
|
||||
publishedAt: null,
|
||||
});
|
||||
|
||||
const document = await buildDocument({
|
||||
text: "This title contains a search term",
|
||||
userId: user.id,
|
||||
@@ -224,14 +217,12 @@ describe("#hooks.slack", () => {
|
||||
it("should return search results with snippet for user through integration mapping", async () => {
|
||||
const { user } = await seed();
|
||||
const serviceTeamId = "slack_team_id";
|
||||
|
||||
await buildIntegration({
|
||||
teamId: user.teamId,
|
||||
settings: {
|
||||
serviceTeamId,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await buildDocument({
|
||||
text: "This title contains a search term",
|
||||
userId: user.id,
|
||||
@@ -257,7 +248,6 @@ describe("#hooks.slack", () => {
|
||||
|
||||
it("should error if incorrect verification token", async () => {
|
||||
const { user, team } = await seed();
|
||||
|
||||
const res = await server.post("/api/hooks.slack", {
|
||||
body: {
|
||||
token: "wrong-verification-token",
|
||||
@@ -269,7 +259,6 @@ describe("#hooks.slack", () => {
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#hooks.interactive", () => {
|
||||
it("should respond with replacement message", async () => {
|
||||
const { user, team } = await seed();
|
||||
@@ -278,15 +267,20 @@ describe("#hooks.interactive", () => {
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const payload = JSON.stringify({
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
user: { id: user.authentications[0].providerId },
|
||||
team: { id: team.authenticationProviders[0].providerId },
|
||||
user: {
|
||||
id: user.authentications[0].providerId,
|
||||
},
|
||||
team: {
|
||||
id: team.authenticationProviders[0].providerId,
|
||||
},
|
||||
callback_id: document.id,
|
||||
});
|
||||
const res = await server.post("/api/hooks.interactive", {
|
||||
body: { payload },
|
||||
body: {
|
||||
payload,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
@@ -302,15 +296,20 @@ describe("#hooks.interactive", () => {
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const payload = JSON.stringify({
|
||||
token: process.env.SLACK_VERIFICATION_TOKEN,
|
||||
user: { id: "unknown-slack-user-id" },
|
||||
team: { id: team.authenticationProviders[0].providerId },
|
||||
user: {
|
||||
id: "unknown-slack-user-id",
|
||||
},
|
||||
team: {
|
||||
id: team.authenticationProviders[0].providerId,
|
||||
},
|
||||
callback_id: document.id,
|
||||
});
|
||||
const res = await server.post("/api/hooks.interactive", {
|
||||
body: { payload },
|
||||
body: {
|
||||
payload,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
@@ -323,11 +322,16 @@ describe("#hooks.interactive", () => {
|
||||
const { user } = await seed();
|
||||
const payload = JSON.stringify({
|
||||
token: "wrong-verification-token",
|
||||
user: { id: user.authentications[0].providerId, name: user.name },
|
||||
user: {
|
||||
id: user.authentications[0].providerId,
|
||||
name: user.name,
|
||||
},
|
||||
callback_id: "doesnt-matter",
|
||||
});
|
||||
const res = await server.post("/api/hooks.interactive", {
|
||||
body: { payload },
|
||||
body: {
|
||||
payload,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import { escapeRegExp } from "lodash";
|
||||
import { AuthenticationError, InvalidRequestError } from "../../errors";
|
||||
import { AuthenticationError, InvalidRequestError } from "@server/errors";
|
||||
import {
|
||||
UserAuthentication,
|
||||
AuthenticationProvider,
|
||||
@@ -11,9 +10,11 @@ import {
|
||||
SearchQuery,
|
||||
Integration,
|
||||
IntegrationAuthentication,
|
||||
} from "../../models";
|
||||
import { presentSlackAttachment } from "../../presenters";
|
||||
import * as Slack from "../../utils/slack";
|
||||
} from "@server/models";
|
||||
import { presentSlackAttachment } from "@server/presenters";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import { assertPresent } from "@server/validation";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// triggered by a user posting a getoutline.com link in Slack
|
||||
@@ -22,13 +23,15 @@ router.post("hooks.unfurl", async (ctx) => {
|
||||
if (challenge) return (ctx.body = ctx.body.challenge);
|
||||
|
||||
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
|
||||
throw new AuthenticationError("Invalid token");
|
||||
throw AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
include: [
|
||||
{
|
||||
where: { providerId: event.user },
|
||||
where: {
|
||||
providerId: event.user,
|
||||
},
|
||||
model: UserAuthentication,
|
||||
as: "authentications",
|
||||
required: true,
|
||||
@@ -36,19 +39,20 @@ router.post("hooks.unfurl", async (ctx) => {
|
||||
],
|
||||
});
|
||||
if (!user) return;
|
||||
|
||||
const auth = await IntegrationAuthentication.findOne({
|
||||
where: { service: "slack", teamId: user.teamId },
|
||||
where: {
|
||||
service: "slack",
|
||||
teamId: user.teamId,
|
||||
},
|
||||
});
|
||||
if (!auth) return;
|
||||
|
||||
// get content for unfurled links
|
||||
let unfurls = {};
|
||||
for (let link of event.links) {
|
||||
const unfurls = {};
|
||||
|
||||
for (const link of event.links) {
|
||||
const id = link.url.substr(link.url.lastIndexOf("/") + 1);
|
||||
const doc = await Document.findByPk(id);
|
||||
if (!doc || doc.teamId !== user.teamId) continue;
|
||||
|
||||
unfurls[link.url] = {
|
||||
title: doc.title,
|
||||
text: doc.getSummary(),
|
||||
@@ -67,27 +71,28 @@ router.post("hooks.unfurl", async (ctx) => {
|
||||
// triggered by interactions with actions, dialogs, message buttons in Slack
|
||||
router.post("hooks.interactive", async (ctx) => {
|
||||
const { payload } = ctx.body;
|
||||
ctx.assertPresent(payload, "payload is required");
|
||||
assertPresent(payload, "payload is required");
|
||||
|
||||
const data = JSON.parse(payload);
|
||||
const { callback_id, token } = data;
|
||||
ctx.assertPresent(token, "token is required");
|
||||
ctx.assertPresent(callback_id, "callback_id is required");
|
||||
|
||||
assertPresent(token, "token is required");
|
||||
assertPresent(callback_id, "callback_id is required");
|
||||
|
||||
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
|
||||
throw new AuthenticationError("Invalid verification token");
|
||||
throw AuthenticationError("Invalid verification token");
|
||||
}
|
||||
|
||||
// we find the document based on the users teamId to ensure access
|
||||
const document = await Document.scope("withCollection").findByPk(
|
||||
data.callback_id
|
||||
);
|
||||
|
||||
if (!document) {
|
||||
throw new InvalidRequestError("Invalid callback_id");
|
||||
throw InvalidRequestError("Invalid callback_id");
|
||||
}
|
||||
|
||||
const team = await Team.findByPk(document.teamId);
|
||||
|
||||
// respond with a public message that will be posted in the original channel
|
||||
ctx.body = {
|
||||
response_type: "in_channel",
|
||||
@@ -106,12 +111,12 @@ router.post("hooks.interactive", async (ctx) => {
|
||||
// triggered by the /outline command in Slack
|
||||
router.post("hooks.slack", async (ctx) => {
|
||||
const { token, team_id, user_id, text = "" } = ctx.body;
|
||||
ctx.assertPresent(token, "token is required");
|
||||
ctx.assertPresent(team_id, "team_id is required");
|
||||
ctx.assertPresent(user_id, "user_id is required");
|
||||
assertPresent(token, "token is required");
|
||||
assertPresent(team_id, "team_id is required");
|
||||
assertPresent(user_id, "user_id is required");
|
||||
|
||||
if (token !== process.env.SLACK_VERIFICATION_TOKEN) {
|
||||
throw new AuthenticationError("Invalid verification token");
|
||||
throw AuthenticationError("Invalid verification token");
|
||||
}
|
||||
|
||||
// Handle "help" command or no input
|
||||
@@ -130,7 +135,6 @@ router.post("hooks.slack", async (ctx) => {
|
||||
}
|
||||
|
||||
let user, team;
|
||||
|
||||
// attempt to find the corresponding team for this request based on the team_id
|
||||
team = await Team.findOne({
|
||||
include: [
|
||||
@@ -153,7 +157,9 @@ router.post("hooks.slack", async (ctx) => {
|
||||
},
|
||||
include: [
|
||||
{
|
||||
where: { teamId: team.id },
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
model: User,
|
||||
as: "user",
|
||||
required: true,
|
||||
@@ -201,14 +207,12 @@ router.post("hooks.slack", async (ctx) => {
|
||||
const options = {
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
// If we were able to map the request to a user then we can use their permissions
|
||||
// to load more documents based on the collections they have access to. Otherwise
|
||||
// just a generic search against team-visible documents is allowed.
|
||||
const { results, totalCount } = user
|
||||
? await Document.searchForUser(user, text, options)
|
||||
: await Document.searchForTeam(team, text, options);
|
||||
|
||||
SearchQuery.create({
|
||||
userId: user ? user.id : null,
|
||||
teamId: team.id,
|
||||
@@ -216,17 +220,16 @@ router.post("hooks.slack", async (ctx) => {
|
||||
query: text,
|
||||
results: totalCount,
|
||||
});
|
||||
|
||||
const haventSignedIn = `(It looks like you haven’t signed in to Outline yet, so results may be limited)`;
|
||||
|
||||
// Map search results to the format expected by the Slack API
|
||||
if (results.length) {
|
||||
const attachments = [];
|
||||
|
||||
for (const result of results) {
|
||||
const queryIsInTitle = !!result.document.title
|
||||
.toLowerCase()
|
||||
.match(escapeRegExp(text.toLowerCase()));
|
||||
|
||||
attachments.push(
|
||||
presentSlackAttachment(
|
||||
result.document,
|
||||
@@ -1,20 +1,18 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 webService from "../../services/web";
|
||||
import { flushdb } from "../../test/support";
|
||||
import webService from "@server/services/web";
|
||||
import { flushdb } from "@server/test/support";
|
||||
|
||||
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");
|
||||
@@ -1,12 +1,9 @@
|
||||
// @flow
|
||||
import Koa from "koa";
|
||||
import bodyParser from "koa-body";
|
||||
import Router from "koa-router";
|
||||
|
||||
import { NotFoundError } from "../../errors";
|
||||
import errorHandling from "../../middlewares/errorHandling";
|
||||
import methodOverride from "../../middlewares/methodOverride";
|
||||
import validation from "../../middlewares/validation";
|
||||
import { NotFoundError } from "@server/errors";
|
||||
import errorHandling from "@server/middlewares/errorHandling";
|
||||
import methodOverride from "@server/middlewares/methodOverride";
|
||||
import apiKeys from "./apiKeys";
|
||||
import attachments from "./attachments";
|
||||
import auth from "./auth";
|
||||
@@ -18,7 +15,6 @@ import fileOperationsRoute from "./fileOperations";
|
||||
import groups from "./groups";
|
||||
import hooks from "./hooks";
|
||||
import integrations from "./integrations";
|
||||
|
||||
import apiWrapper from "./middlewares/apiWrapper";
|
||||
import editor from "./middlewares/editor";
|
||||
import notificationSettings from "./notificationSettings";
|
||||
@@ -37,11 +33,12 @@ api.use(errorHandling());
|
||||
api.use(
|
||||
bodyParser({
|
||||
multipart: true,
|
||||
formidable: { maxFieldsSize: 10 * 1024 * 1024 },
|
||||
formidable: {
|
||||
maxFieldsSize: 10 * 1024 * 1024,
|
||||
},
|
||||
})
|
||||
);
|
||||
api.use(methodOverride());
|
||||
api.use(validation());
|
||||
api.use(apiWrapper());
|
||||
api.use(editor());
|
||||
|
||||
@@ -66,7 +63,7 @@ router.use("/", groups.routes());
|
||||
router.use("/", fileOperationsRoute.routes());
|
||||
|
||||
router.post("*", (ctx) => {
|
||||
ctx.throw(new NotFoundError("Endpoint not found"));
|
||||
ctx.throw(NotFoundError("Endpoint not found"));
|
||||
});
|
||||
|
||||
// Router is embedded in a Koa application wrapper, because koa-router does not
|
||||
@@ -1,28 +1,30 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Event } from "../../models";
|
||||
import Integration from "../../models/Integration";
|
||||
import policy from "../../policies";
|
||||
import { presentIntegration } from "../../presenters";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Event } from "@server/models";
|
||||
import Integration from "@server/models/Integration";
|
||||
import policy from "@server/policies";
|
||||
import { presentIntegration } from "@server/presenters";
|
||||
import { assertSort, assertUuid } from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const { authorize } = policy;
|
||||
const router = new Router();
|
||||
|
||||
router.post("integrations.list", auth(), pagination(), async (ctx) => {
|
||||
let { sort = "updatedAt", direction } = ctx.body;
|
||||
let { direction } = ctx.body;
|
||||
const { sort = "updatedAt" } = ctx.body;
|
||||
if (direction !== "ASC") direction = "DESC";
|
||||
ctx.assertSort(sort, Integration);
|
||||
assertSort(sort, Integration);
|
||||
|
||||
const user = ctx.state.user;
|
||||
const integrations = await Integration.findAll({
|
||||
where: { teamId: user.teamId },
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: integrations.map(presentIntegration),
|
||||
@@ -31,14 +33,12 @@ router.post("integrations.list", auth(), pagination(), async (ctx) => {
|
||||
|
||||
router.post("integrations.delete", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const integration = await Integration.findByPk(id);
|
||||
authorize(user, "delete", integration);
|
||||
|
||||
await integration.destroy();
|
||||
|
||||
await Event.create({
|
||||
name: "integrations.delete",
|
||||
modelId: integration.id,
|
||||
@@ -46,7 +46,6 @@ router.post("integrations.delete", auth(), async (ctx) => {
|
||||
actorId: user.id,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -1,23 +1,20 @@
|
||||
// @flow
|
||||
import stream from "stream";
|
||||
import { type Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
|
||||
export default function apiWrapper() {
|
||||
return async function apiWrapperMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
await next();
|
||||
|
||||
const ok = ctx.status < 400;
|
||||
|
||||
if (
|
||||
typeof ctx.body !== "string" &&
|
||||
!(ctx.body instanceof stream.Readable)
|
||||
) {
|
||||
// $FlowFixMe
|
||||
ctx.body = {
|
||||
// $FlowFixMe
|
||||
// @ts-expect-error ts-migrate(2698) FIXME: Spread types may only be created from object types... Remove this comment to see the full error message
|
||||
...ctx.body,
|
||||
status: ctx.status,
|
||||
ok,
|
||||
@@ -1,11 +1,14 @@
|
||||
// @flow
|
||||
import { type Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
import pkg from "rich-markdown-editor/package.json";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'semv... Remove this comment to see the full error message
|
||||
import semver from "semver";
|
||||
import { EditorUpdateError } from "../../../errors";
|
||||
import { EditorUpdateError } from "@server/errors";
|
||||
|
||||
export default function editor() {
|
||||
return async function editorMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||
return async function editorMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
const clientVersion = ctx.headers["x-editor-version"];
|
||||
|
||||
// If the editor version on the client is behind the current version being
|
||||
@@ -19,9 +22,10 @@ export default function editor() {
|
||||
parsedClientVersion.major < parsedCurrentVersion.major ||
|
||||
parsedClientVersion.minor < parsedCurrentVersion.minor
|
||||
) {
|
||||
throw new EditorUpdateError();
|
||||
throw EditorUpdateError();
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
@@ -1,56 +1,66 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 webService from "../../../services/web";
|
||||
import { flushdb, seed } from "../../../test/support";
|
||||
import webService from "@server/services/web";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
|
||||
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();
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: { token: user.getJwtToken(), limit: 1, offset: 1 },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
limit: 1,
|
||||
offset: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
|
||||
it("should not allow negative limit", async () => {
|
||||
const { user } = await seed();
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: { token: user.getJwtToken(), limit: -1 },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
limit: -1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
|
||||
it("should not allow non-integer limit", async () => {
|
||||
const { user } = await seed();
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: { token: user.getJwtToken(), limit: "blah" },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
limit: "blah",
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
|
||||
it("should not allow negative offset", async () => {
|
||||
const { user } = await seed();
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: { token: user.getJwtToken(), offset: -1 },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
offset: -1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
|
||||
it("should not allow non-integer offset", async () => {
|
||||
const { user } = await seed();
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: { token: user.getJwtToken(), offset: "blah" },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
offset: "blah",
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
import querystring from "querystring";
|
||||
import { type Context } from "koa";
|
||||
import { InvalidRequestError } from "../../../errors";
|
||||
import { Context } from "koa";
|
||||
import { InvalidRequestError } from "@server/errors";
|
||||
|
||||
export default function pagination(options?: Object) {
|
||||
export default function pagination(options?: Record<string, any>) {
|
||||
return async function paginationMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
const opts = {
|
||||
defaultLimit: 15,
|
||||
@@ -14,53 +13,46 @@ export default function pagination(options?: Object) {
|
||||
maxLimit: 100,
|
||||
...options,
|
||||
};
|
||||
|
||||
let query = ctx.request.query;
|
||||
let body = ctx.request.body;
|
||||
|
||||
// $FlowFixMe
|
||||
const query = ctx.request.query;
|
||||
const body = ctx.request.body;
|
||||
let limit = query.limit || body.limit;
|
||||
// $FlowFixMe
|
||||
let offset = query.offset || body.offset;
|
||||
|
||||
if (limit && isNaN(limit)) {
|
||||
throw new InvalidRequestError(`Pagination limit must be a valid number`);
|
||||
throw InvalidRequestError(`Pagination limit must be a valid number`);
|
||||
}
|
||||
|
||||
if (offset && isNaN(offset)) {
|
||||
throw new InvalidRequestError(`Pagination offset must be a valid number`);
|
||||
throw InvalidRequestError(`Pagination offset must be a valid number`);
|
||||
}
|
||||
|
||||
limit = parseInt(limit || opts.defaultLimit, 10);
|
||||
offset = parseInt(offset || opts.defaultOffset, 10);
|
||||
|
||||
if (limit > opts.maxLimit) {
|
||||
throw new InvalidRequestError(
|
||||
throw InvalidRequestError(
|
||||
`Pagination limit is too large (max ${opts.maxLimit})`
|
||||
);
|
||||
}
|
||||
|
||||
if (limit <= 0) {
|
||||
throw new InvalidRequestError(`Pagination limit must be greater than 0`);
|
||||
throw InvalidRequestError(`Pagination limit must be greater than 0`);
|
||||
}
|
||||
|
||||
if (offset < 0) {
|
||||
throw new InvalidRequestError(
|
||||
throw InvalidRequestError(
|
||||
`Pagination offset must be greater than or equal to 0`
|
||||
);
|
||||
}
|
||||
|
||||
/* $FlowFixMeNowPlease This comment suppresses an error found when upgrading
|
||||
* flow-bin@0.104.0. To view the error, delete this comment and run Flow. */
|
||||
ctx.state.pagination = {
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
|
||||
// $FlowFixMe
|
||||
query.limit = ctx.state.pagination.limit;
|
||||
// $FlowFixMe
|
||||
query.offset = ctx.state.pagination.offset + query.limit;
|
||||
|
||||
/* $FlowFixMeNowPlease This comment suppresses an error found when upgrading
|
||||
* flow-bin@0.104.0. To view the error, delete this comment and run Flow. */
|
||||
ctx.state.pagination.nextPath = `/api${
|
||||
ctx.request.path
|
||||
}?${querystring.stringify(query)}`;
|
||||
@@ -1,21 +1,19 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Team, NotificationSetting } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { presentNotificationSetting } from "../../presenters";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Team, NotificationSetting } from "@server/models";
|
||||
import policy 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;
|
||||
ctx.assertPresent(event, "event is required");
|
||||
assertPresent(event, "event is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
authorize(user, "createNotificationSetting", user.team);
|
||||
|
||||
const [setting] = await NotificationSetting.findOrCreate({
|
||||
where: {
|
||||
userId: user.id,
|
||||
@@ -23,7 +21,6 @@ router.post("notificationSettings.create", auth(), async (ctx) => {
|
||||
event,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentNotificationSetting(setting),
|
||||
};
|
||||
@@ -36,7 +33,6 @@ router.post("notificationSettings.list", auth(), async (ctx) => {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: settings.map(presentNotificationSetting),
|
||||
};
|
||||
@@ -44,14 +40,12 @@ router.post("notificationSettings.list", auth(), async (ctx) => {
|
||||
|
||||
router.post("notificationSettings.delete", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const setting = await NotificationSetting.findByPk(id);
|
||||
authorize(user, "delete", setting);
|
||||
|
||||
await setting.destroy();
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -59,8 +53,8 @@ router.post("notificationSettings.delete", auth(), async (ctx) => {
|
||||
|
||||
router.post("notificationSettings.unsubscribe", async (ctx) => {
|
||||
const { id, token } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
ctx.assertPresent(token, "token is required");
|
||||
assertUuid(id, "id is required");
|
||||
assertPresent(token, "token is required");
|
||||
|
||||
const setting = await NotificationSetting.findByPk(id, {
|
||||
include: [
|
||||
@@ -1,21 +1,18 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 { Revision } from "../../models";
|
||||
import webService from "../../services/web";
|
||||
import { buildDocument, buildUser } from "../../test/factories";
|
||||
import { flushdb, seed } from "../../test/support";
|
||||
import { Revision } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
|
||||
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();
|
||||
const revision = await Revision.createFromDocument(document);
|
||||
|
||||
const res = await server.post("/api/revisions.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -23,7 +20,6 @@ describe("#revisions.info", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).not.toEqual(document.id);
|
||||
expect(body.data.title).toEqual(document.title);
|
||||
@@ -32,7 +28,6 @@ describe("#revisions.info", () => {
|
||||
it("should require authorization", async () => {
|
||||
const document = await buildDocument();
|
||||
const revision = await Revision.createFromDocument(document);
|
||||
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/revisions.info", {
|
||||
body: {
|
||||
@@ -43,12 +38,10 @@ describe("#revisions.info", () => {
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#revisions.list", () => {
|
||||
it("should return a document's revisions", async () => {
|
||||
const { user, document } = await seed();
|
||||
await Revision.createFromDocument(document);
|
||||
|
||||
const res = await server.post("/api/revisions.list", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -56,7 +49,6 @@ describe("#revisions.list", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).not.toEqual(document.id);
|
||||
@@ -66,14 +58,14 @@ describe("#revisions.list", () => {
|
||||
it("should not return revisions for document in collection not a member of", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
await Revision.createFromDocument(document);
|
||||
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/revisions.list", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import { NotFoundError } from "../../errors";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Document, Revision } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { presentRevision } from "../../presenters";
|
||||
import { NotFoundError } from "@server/errors";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Document, Revision } from "@server/models";
|
||||
import policy 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) => {
|
||||
let { id } = ctx.body;
|
||||
ctx.assertPresent(id, "id is required");
|
||||
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const revision = await Revision.findByPk(id);
|
||||
|
||||
if (!revision) {
|
||||
throw new NotFoundError();
|
||||
throw NotFoundError();
|
||||
}
|
||||
|
||||
const document = await Document.findByPk(revision.documentId, {
|
||||
@@ -32,26 +32,29 @@ router.post("revisions.info", auth(), async (ctx) => {
|
||||
});
|
||||
|
||||
router.post("revisions.list", auth(), pagination(), async (ctx) => {
|
||||
let { documentId, sort = "updatedAt", direction } = ctx.body;
|
||||
let { direction } = ctx.body;
|
||||
const { documentId, sort = "updatedAt" } = ctx.body;
|
||||
if (direction !== "ASC") direction = "DESC";
|
||||
ctx.assertSort(sort, Revision);
|
||||
ctx.assertPresent(documentId, "documentId is required");
|
||||
assertSort(sort, Revision);
|
||||
assertPresent(documentId, "documentId is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
const revisions = await Revision.findAll({
|
||||
where: { documentId: document.id },
|
||||
where: {
|
||||
documentId: document.id,
|
||||
},
|
||||
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 'revision' implicitly has an 'any' type.
|
||||
revisions.map((revision) => presentRevision(revision))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
@@ -1,16 +1,14 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 { CollectionUser } from "../../models";
|
||||
import webService from "../../services/web";
|
||||
import { buildUser, buildDocument, buildShare } from "../../test/factories";
|
||||
import { flushdb, seed } from "../../test/support";
|
||||
import { CollectionUser } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildDocument, buildShare } from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
|
||||
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();
|
||||
@@ -25,10 +23,11 @@ describe("#shares.list", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.list", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(share.id);
|
||||
@@ -43,12 +42,12 @@ describe("#shares.list", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
await share.revoke(user.id);
|
||||
|
||||
const res = await server.post("/api/shares.list", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
@@ -61,12 +60,12 @@ describe("#shares.list", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.list", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
@@ -78,14 +77,13 @@ describe("#shares.list", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await document.delete(user.id);
|
||||
|
||||
const res = await server.post("/api/shares.list", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
@@ -98,10 +96,11 @@ describe("#shares.list", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.list", {
|
||||
body: { token: admin.getJwtToken() },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(share.id);
|
||||
@@ -115,15 +114,14 @@ describe("#shares.list", () => {
|
||||
teamId: admin.teamId,
|
||||
userId: admin.id,
|
||||
});
|
||||
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
const res = await server.post("/api/shares.list", {
|
||||
body: { token: admin.getJwtToken() },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
@@ -131,20 +129,20 @@ describe("#shares.list", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/shares.list");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#shares.create", () => {
|
||||
it("should allow creating a share record for document", async () => {
|
||||
const { user, document } = await seed();
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.published).toBe(false);
|
||||
expect(body.data.documentTitle).toBe(document.title);
|
||||
@@ -153,26 +151,28 @@ describe("#shares.create", () => {
|
||||
it("should allow creating a share record with read-only permissions but no publishing", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = null;
|
||||
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const response = await server.post("/api/shares.update", {
|
||||
body: { token: user.getJwtToken(), id: body.data.id, published: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: body.data.id,
|
||||
published: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(403);
|
||||
});
|
||||
|
||||
@@ -185,10 +185,12 @@ describe("#shares.create", () => {
|
||||
});
|
||||
await share.revoke();
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).not.toEqual(share.id);
|
||||
expect(body.data.documentTitle).toBe(document.title);
|
||||
@@ -202,53 +204,70 @@ describe("#shares.create", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(share.id);
|
||||
});
|
||||
|
||||
it("should allow creating a share record if team sharing disabled but not publishing", async () => {
|
||||
const { user, document, team } = await seed();
|
||||
await team.update({ sharing: false });
|
||||
await team.update({
|
||||
sharing: false,
|
||||
});
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const response = await server.post("/api/shares.update", {
|
||||
body: { token: user.getJwtToken(), id: body.data.id, published: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: body.data.id,
|
||||
published: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("should allow creating a share record if collection sharing disabled but not publishing", async () => {
|
||||
const { user, collection, document } = await seed();
|
||||
await collection.update({ sharing: false });
|
||||
await collection.update({
|
||||
sharing: false,
|
||||
});
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const response = await server.post("/api/shares.update", {
|
||||
body: { token: user.getJwtToken(), id: body.data.id, published: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: body.data.id,
|
||||
published: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const { document } = await seed();
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: { documentId: document.id },
|
||||
body: {
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -257,12 +276,14 @@ describe("#shares.create", () => {
|
||||
const { document } = await seed();
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/shares.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#shares.info", () => {
|
||||
it("should allow reading share by id", async () => {
|
||||
const { user, document } = await seed();
|
||||
@@ -271,12 +292,13 @@ describe("#shares.info", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: { token: user.getJwtToken(), id: share.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(share.id);
|
||||
expect(body.data.createdBy.id).toBe(user.id);
|
||||
@@ -284,20 +306,22 @@ describe("#shares.info", () => {
|
||||
|
||||
it("should allow reading share created by deleted user", async () => {
|
||||
const { user, document } = await seed();
|
||||
const author = await buildUser({ teamId: user.teamId });
|
||||
const author = await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const share = await buildShare({
|
||||
documentId: document.id,
|
||||
teamId: author.teamId,
|
||||
userId: author.id,
|
||||
});
|
||||
|
||||
await author.destroy();
|
||||
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: { token: user.getJwtToken(), id: share.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(share.id);
|
||||
expect(body.data.createdBy.id).toBe(author.id);
|
||||
@@ -310,12 +334,13 @@ describe("#shares.info", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(share.id);
|
||||
expect(body.data.published).toBe(true);
|
||||
@@ -332,10 +357,12 @@ describe("#shares.info", () => {
|
||||
userId: admin.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(share.id);
|
||||
expect(body.data.published).toBe(true);
|
||||
@@ -350,7 +377,10 @@ describe("#shares.info", () => {
|
||||
});
|
||||
await share.revoke();
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(204);
|
||||
});
|
||||
@@ -364,11 +394,13 @@ describe("#shares.info", () => {
|
||||
});
|
||||
await document.delete(user.id);
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(204);
|
||||
});
|
||||
|
||||
describe("apiVersion=2", () => {
|
||||
it("should allow reading share by documentId", async () => {
|
||||
const { user, document } = await seed();
|
||||
@@ -377,7 +409,6 @@ describe("#shares.info", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -386,13 +417,11 @@ describe("#shares.info", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.shares.length).toBe(1);
|
||||
expect(body.data.shares[0].id).toBe(share.id);
|
||||
expect(body.data.shares[0].published).toBe(true);
|
||||
});
|
||||
|
||||
it("should return share for parent document with includeChildDocuments=true", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
const childDocument = await buildDocument({
|
||||
@@ -406,9 +435,7 @@ describe("#shares.info", () => {
|
||||
userId: user.id,
|
||||
includeChildDocuments: true,
|
||||
});
|
||||
|
||||
await collection.addDocumentToStructure(childDocument, 0);
|
||||
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -417,7 +444,6 @@ describe("#shares.info", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.shares.length).toBe(1);
|
||||
expect(body.data.shares[0].id).toBe(share.id);
|
||||
@@ -427,7 +453,6 @@ describe("#shares.info", () => {
|
||||
expect(body.policies.length).toBe(1);
|
||||
expect(body.policies[0].abilities.update).toBe(true);
|
||||
});
|
||||
|
||||
it("should not return share for parent document with includeChildDocuments=false", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
const childDocument = await buildDocument({
|
||||
@@ -441,9 +466,7 @@ describe("#shares.info", () => {
|
||||
userId: user.id,
|
||||
includeChildDocuments: false,
|
||||
});
|
||||
|
||||
await collection.addDocumentToStructure(childDocument, 0);
|
||||
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -451,10 +474,8 @@ describe("#shares.info", () => {
|
||||
apiVersion: 2,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(204);
|
||||
});
|
||||
|
||||
it("should return shares for parent document and current document", async () => {
|
||||
const { user, document, collection } = await seed();
|
||||
const childDocument = await buildDocument({
|
||||
@@ -474,9 +495,7 @@ describe("#shares.info", () => {
|
||||
userId: user.id,
|
||||
includeChildDocuments: true,
|
||||
});
|
||||
|
||||
await collection.addDocumentToStructure(childDocument, 0);
|
||||
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
@@ -485,14 +504,12 @@ describe("#shares.info", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.shares.length).toBe(2);
|
||||
expect(body.data.shares[0].id).toBe(share.id);
|
||||
expect(body.data.shares[0].includeChildDocuments).toBe(false);
|
||||
expect(body.data.shares[0].documentId).toBe(childDocument.id);
|
||||
expect(body.data.shares[0].published).toBe(true);
|
||||
|
||||
expect(body.data.shares[1].id).toBe(share2.id);
|
||||
expect(body.data.shares[1].documentId).toBe(document.id);
|
||||
expect(body.data.shares[1].published).toBe(true);
|
||||
@@ -508,10 +525,11 @@ describe("#shares.info", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: { id: share.id },
|
||||
body: {
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -525,12 +543,14 @@ describe("#shares.info", () => {
|
||||
userId: admin.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.info", {
|
||||
body: { token: user.getJwtToken(), id: share.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#shares.update", () => {
|
||||
it("should allow user to update a share", async () => {
|
||||
const { user, document } = await seed();
|
||||
@@ -538,12 +558,14 @@ describe("#shares.update", () => {
|
||||
documentId: document.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.update", {
|
||||
body: { token: user.getJwtToken(), id: share.id, published: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
published: true,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(share.id);
|
||||
expect(body.data.published).toBe(true);
|
||||
@@ -556,12 +578,14 @@ describe("#shares.update", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.update", {
|
||||
body: { token: user.getJwtToken(), id: share.id, published: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
published: true,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(share.id);
|
||||
expect(body.data.published).toBe(true);
|
||||
@@ -574,12 +598,14 @@ describe("#shares.update", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.update", {
|
||||
body: { token: admin.getJwtToken(), id: share.id, published: true },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: share.id,
|
||||
published: true,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBe(share.id);
|
||||
expect(body.data.published).toBe(true);
|
||||
@@ -593,10 +619,12 @@ describe("#shares.update", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.update", {
|
||||
body: { id: share.id, published: true },
|
||||
body: {
|
||||
id: share.id,
|
||||
published: true,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -610,12 +638,15 @@ describe("#shares.update", () => {
|
||||
userId: admin.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.update", {
|
||||
body: { token: user.getJwtToken(), id: share.id, published: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
published: true,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#shares.revoke", () => {
|
||||
it("should allow author to revoke a share", async () => {
|
||||
const { user, document } = await seed();
|
||||
@@ -624,9 +655,11 @@ describe("#shares.revoke", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.revoke", {
|
||||
body: { token: user.getJwtToken(), id: share.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
@@ -638,11 +671,12 @@ describe("#shares.revoke", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await document.delete(user.id);
|
||||
|
||||
const res = await server.post("/api/shares.revoke", {
|
||||
body: { token: user.getJwtToken(), id: share.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(404);
|
||||
});
|
||||
@@ -654,9 +688,11 @@ describe("#shares.revoke", () => {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/shares.revoke", {
|
||||
body: { token: admin.getJwtToken(), id: share.id },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
@@ -669,10 +705,11 @@ describe("#shares.revoke", () => {
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.revoke", {
|
||||
body: { id: share.id },
|
||||
body: {
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -686,7 +723,10 @@ describe("#shares.revoke", () => {
|
||||
userId: admin.id,
|
||||
});
|
||||
const res = await server.post("/api/shares.revoke", {
|
||||
body: { token: user.getJwtToken(), id: share.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: share.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
@@ -1,35 +1,39 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import Sequelize from "sequelize";
|
||||
import { NotFoundError } from "../../errors";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Document, User, Event, Share, Team, Collection } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { presentShare, presentPolicies } from "../../presenters";
|
||||
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 { 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();
|
||||
|
||||
// @ts-expect-error ts-migrate(7030) FIXME: Not all code paths return a value.
|
||||
router.post("shares.info", auth(), async (ctx) => {
|
||||
const { id, documentId, apiVersion } = ctx.body;
|
||||
ctx.assertUuid(id || documentId, "id or documentId is required");
|
||||
assertUuid(id || documentId, "id or documentId is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
let shares = [];
|
||||
let share = await Share.scope({
|
||||
const shares = [];
|
||||
const share = await Share.scope({
|
||||
method: ["withCollection", user.id],
|
||||
}).findOne({
|
||||
where: id
|
||||
? {
|
||||
id,
|
||||
revokedAt: { [Op.eq]: null },
|
||||
revokedAt: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
}
|
||||
: {
|
||||
documentId,
|
||||
teamId: user.teamId,
|
||||
revokedAt: { [Op.eq]: null },
|
||||
revokedAt: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,7 +44,6 @@ router.post("shares.info", auth(), async (ctx) => {
|
||||
}
|
||||
|
||||
authorize(user, "read", share);
|
||||
|
||||
ctx.body = {
|
||||
data: presentShare(share, user.isAdmin),
|
||||
policies: presentPolicies(user, [share]),
|
||||
@@ -60,7 +63,6 @@ router.post("shares.info", auth(), async (ctx) => {
|
||||
documentId
|
||||
);
|
||||
const parentIds = document?.collection?.getDocumentParents(documentId);
|
||||
|
||||
const parentShare = parentIds
|
||||
? await Share.scope({
|
||||
method: ["withCollection", user.id],
|
||||
@@ -68,7 +70,9 @@ router.post("shares.info", auth(), async (ctx) => {
|
||||
where: {
|
||||
documentId: parentIds,
|
||||
teamId: user.teamId,
|
||||
revokedAt: { [Op.eq]: null },
|
||||
revokedAt: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
includeChildDocuments: true,
|
||||
published: true,
|
||||
},
|
||||
@@ -94,16 +98,19 @@ router.post("shares.info", auth(), async (ctx) => {
|
||||
});
|
||||
|
||||
router.post("shares.list", auth(), pagination(), async (ctx) => {
|
||||
let { sort = "updatedAt", direction } = ctx.body;
|
||||
let { direction } = ctx.body;
|
||||
const { sort = "updatedAt" } = ctx.body;
|
||||
if (direction !== "ASC") direction = "DESC";
|
||||
ctx.assertSort(sort, Share);
|
||||
assertSort(sort, Share);
|
||||
|
||||
const user = ctx.state.user;
|
||||
const where = {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
published: true,
|
||||
revokedAt: { [Op.eq]: null },
|
||||
revokedAt: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
};
|
||||
|
||||
if (user.isAdmin) {
|
||||
@@ -146,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),
|
||||
};
|
||||
@@ -156,17 +163,15 @@ router.post("shares.list", auth(), pagination(), async (ctx) => {
|
||||
|
||||
router.post("shares.update", auth(), async (ctx) => {
|
||||
const { id, includeChildDocuments, published } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const { user } = ctx.state;
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
authorize(user, "share", team);
|
||||
|
||||
// fetch the share with document and collection.
|
||||
const share = await Share.scope({
|
||||
method: ["withCollection", user.id],
|
||||
}).findByPk(id);
|
||||
|
||||
authorize(user, "update", share);
|
||||
|
||||
if (published !== undefined) {
|
||||
@@ -185,17 +190,17 @@ router.post("shares.update", auth(), async (ctx) => {
|
||||
}
|
||||
|
||||
await share.save();
|
||||
|
||||
await Event.create({
|
||||
name: "shares.update",
|
||||
documentId: share.documentId,
|
||||
modelId: share.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: { published },
|
||||
data: {
|
||||
published,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentShare(share, user.isAdmin),
|
||||
policies: presentPolicies(user, [share]),
|
||||
@@ -204,10 +209,12 @@ router.post("shares.update", auth(), async (ctx) => {
|
||||
|
||||
router.post("shares.create", auth(), async (ctx) => {
|
||||
const { documentId } = ctx.body;
|
||||
ctx.assertPresent(documentId, "documentId is required");
|
||||
assertPresent(documentId, "documentId is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||
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);
|
||||
@@ -231,7 +238,9 @@ router.post("shares.create", auth(), async (ctx) => {
|
||||
modelId: share.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: { name: document.title },
|
||||
data: {
|
||||
name: document.title,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
}
|
||||
@@ -239,7 +248,6 @@ router.post("shares.create", auth(), async (ctx) => {
|
||||
share.team = team;
|
||||
share.user = user;
|
||||
share.document = document;
|
||||
|
||||
ctx.body = {
|
||||
data: presentShare(share),
|
||||
policies: presentPolicies(user, [share]),
|
||||
@@ -248,19 +256,18 @@ router.post("shares.create", auth(), async (ctx) => {
|
||||
|
||||
router.post("shares.revoke", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, "id is required");
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const share = await Share.findByPk(id);
|
||||
authorize(user, "revoke", share);
|
||||
|
||||
const document = await Document.findByPk(share.documentId);
|
||||
|
||||
if (!document) {
|
||||
throw new NotFoundError();
|
||||
throw NotFoundError();
|
||||
}
|
||||
|
||||
await share.revoke(user.id);
|
||||
|
||||
await Event.create({
|
||||
name: "shares.revoke",
|
||||
documentId: document.id,
|
||||
@@ -268,10 +275,11 @@ router.post("shares.revoke", auth(), async (ctx) => {
|
||||
modelId: share.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: { name: document.title },
|
||||
data: {
|
||||
name: document.title,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -1,22 +1,22 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 webService from "../../services/web";
|
||||
import webService from "@server/services/web";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
|
||||
import { flushdb, seed } from "../../test/support";
|
||||
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();
|
||||
const res = await server.post("/api/team.update", {
|
||||
body: { token: admin.getJwtToken(), name: "New name" },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
name: "New name",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toEqual("New name");
|
||||
});
|
||||
@@ -24,15 +24,18 @@ describe("#team.update", () => {
|
||||
it("should only allow member,viewer or admin as default role", async () => {
|
||||
const { admin } = await seed();
|
||||
const res = await server.post("/api/team.update", {
|
||||
body: { token: admin.getJwtToken(), defaultUserRole: "New name" },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
defaultUserRole: "New name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
|
||||
const successRes = await server.post("/api/team.update", {
|
||||
body: { token: admin.getJwtToken(), defaultUserRole: "viewer" },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
defaultUserRole: "viewer",
|
||||
},
|
||||
});
|
||||
|
||||
const body = await successRes.json();
|
||||
expect(successRes.status).toEqual(200);
|
||||
expect(body.data.defaultUserRole).toBe("viewer");
|
||||
@@ -41,10 +44,12 @@ describe("#team.update", () => {
|
||||
it("should allow identical team details", async () => {
|
||||
const { admin, team } = await seed();
|
||||
const res = await server.post("/api/team.update", {
|
||||
body: { token: admin.getJwtToken(), name: team.name },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
name: team.name,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toEqual(team.name);
|
||||
});
|
||||
@@ -52,7 +57,10 @@ describe("#team.update", () => {
|
||||
it("should require admin", async () => {
|
||||
const { user } = await seed();
|
||||
const res = await server.post("/api/team.update", {
|
||||
body: { token: user.getJwtToken(), name: "New name" },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
name: "New name",
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
@@ -1,10 +1,8 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Event, Team } from "../../models";
|
||||
|
||||
import policy from "../../policies";
|
||||
import { presentTeam, presentPolicies } from "../../presenters";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Event, Team } from "@server/models";
|
||||
import policy from "@server/policies";
|
||||
import { presentTeam, presentPolicies } from "@server/presenters";
|
||||
|
||||
const { authorize } = policy;
|
||||
const router = new Router();
|
||||
@@ -33,16 +31,17 @@ router.post("team.update", auth(), async (ctx) => {
|
||||
if (documentEmbeds !== undefined) team.documentEmbeds = documentEmbeds;
|
||||
if (guestSignin !== undefined) team.guestSignin = guestSignin;
|
||||
if (avatarUrl !== undefined) team.avatarUrl = avatarUrl;
|
||||
|
||||
if (collaborativeEditing !== undefined) {
|
||||
team.collaborativeEditing = collaborativeEditing;
|
||||
}
|
||||
|
||||
if (defaultUserRole !== undefined) {
|
||||
team.defaultUserRole = defaultUserRole;
|
||||
}
|
||||
|
||||
const changes = team.changed();
|
||||
const data = {};
|
||||
|
||||
await team.save();
|
||||
|
||||
if (changes) {
|
||||
@@ -1,27 +1,24 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 webService from "../../services/web";
|
||||
import webService from "@server/services/web";
|
||||
import { buildTeam, buildAdmin, buildUser } from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
|
||||
import { buildTeam, buildAdmin, buildUser } from "../../test/factories";
|
||||
|
||||
import { flushdb, seed } from "../../test/support";
|
||||
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({ name: "Tester" });
|
||||
|
||||
const user = await buildUser({
|
||||
name: "Tester",
|
||||
});
|
||||
// suspended user should not be returned
|
||||
await buildUser({
|
||||
name: "Tester",
|
||||
teamId: user.teamId,
|
||||
suspendedAt: new Date(),
|
||||
});
|
||||
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: {
|
||||
query: "test",
|
||||
@@ -29,20 +26,20 @@ describe("#users.list", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it("should allow filtering to suspended users", async () => {
|
||||
const user = await buildUser({ name: "Tester" });
|
||||
const user = await buildUser({
|
||||
name: "Tester",
|
||||
});
|
||||
await buildUser({
|
||||
name: "Tester",
|
||||
teamId: user.teamId,
|
||||
suspendedAt: new Date(),
|
||||
});
|
||||
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: {
|
||||
query: "test",
|
||||
@@ -51,19 +48,19 @@ describe("#users.list", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should allow filtering to invited", async () => {
|
||||
const user = await buildUser({ name: "Tester" });
|
||||
const user = await buildUser({
|
||||
name: "Tester",
|
||||
});
|
||||
await buildUser({
|
||||
name: "Tester",
|
||||
teamId: user.teamId,
|
||||
lastActiveAt: null,
|
||||
});
|
||||
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: {
|
||||
query: "test",
|
||||
@@ -72,19 +69,18 @@ describe("#users.list", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should return teams paginated user list", async () => {
|
||||
const { admin, user } = await seed();
|
||||
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: { token: admin.getJwtToken() },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(2);
|
||||
expect(body.data[0].id).toEqual(user.id);
|
||||
@@ -94,10 +90,11 @@ describe("#users.list", () => {
|
||||
it("should require admin for detailed info", async () => {
|
||||
const { user, admin } = await seed();
|
||||
const res = await server.post("/api/users.list", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(2);
|
||||
expect(body.data[0].email).toEqual(undefined);
|
||||
@@ -106,15 +103,15 @@ 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();
|
||||
const res = await server.post("/api/users.info", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toEqual(user.id);
|
||||
expect(body.data.name).toEqual(user.name);
|
||||
@@ -123,16 +120,19 @@ describe("#users.info", () => {
|
||||
|
||||
it("should return user with permission", async () => {
|
||||
const user = await buildUser();
|
||||
const another = await buildUser({ teamId: user.teamId });
|
||||
const another = await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/users.info", {
|
||||
body: { token: user.getJwtToken(), id: another.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: another.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toEqual(another.id);
|
||||
expect(body.data.name).toEqual(another.name);
|
||||
|
||||
// no emails of other users
|
||||
expect(body.data.email).toEqual(undefined);
|
||||
});
|
||||
@@ -141,9 +141,11 @@ describe("#users.info", () => {
|
||||
const user = await buildUser();
|
||||
const another = await buildUser();
|
||||
const res = await server.post("/api/users.info", {
|
||||
body: { token: user.getJwtToken(), id: another.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: another.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
@@ -152,14 +154,19 @@ describe("#users.info", () => {
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#users.invite", () => {
|
||||
it("should return sent invites", async () => {
|
||||
const user = await buildAdmin();
|
||||
const res = await server.post("/api/users.invite", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
invites: [{ email: "test@example.com", name: "Test", role: "member" }],
|
||||
invites: [
|
||||
{
|
||||
email: "test@example.com",
|
||||
name: "Test",
|
||||
role: "member",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
@@ -172,7 +179,11 @@ describe("#users.invite", () => {
|
||||
const res = await server.post("/api/users.invite", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
invites: { email: "test@example.com", name: "Test", role: "member" },
|
||||
invites: {
|
||||
email: "test@example.com",
|
||||
name: "Test",
|
||||
role: "member",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(400);
|
||||
@@ -183,7 +194,13 @@ describe("#users.invite", () => {
|
||||
const res = await server.post("/api/users.invite", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
invites: [{ email: "test@example.com", name: "Test", role: "member" }],
|
||||
invites: [
|
||||
{
|
||||
email: "test@example.com",
|
||||
name: "Test",
|
||||
role: "member",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
@@ -194,7 +211,13 @@ describe("#users.invite", () => {
|
||||
const res = await server.post("/api/users.invite", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
invites: [{ email: "test@example.com", name: "Test", role: "admin" }],
|
||||
invites: [
|
||||
{
|
||||
email: "test@example.com",
|
||||
name: "Test",
|
||||
role: "admin",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
@@ -209,7 +232,13 @@ describe("#users.invite", () => {
|
||||
const res = await server.post("/api/users.invite", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
invites: [{ email: "test@example.com", name: "Test", role: "viewer" }],
|
||||
invites: [
|
||||
{
|
||||
email: "test@example.com",
|
||||
name: "Test",
|
||||
role: "viewer",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
@@ -225,7 +254,11 @@ describe("#users.invite", () => {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
invites: [
|
||||
{ email: "test@example.com", name: "Test", role: "arbitary" },
|
||||
{
|
||||
email: "test@example.com",
|
||||
name: "Test",
|
||||
role: "arbitary",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -241,32 +274,42 @@ describe("#users.invite", () => {
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#users.delete", () => {
|
||||
it("should not allow deleting without confirmation", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/users.delete", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
|
||||
it("should not allow deleting last admin if many users", async () => {
|
||||
const user = await buildAdmin();
|
||||
await buildUser({ teamId: user.teamId, isAdmin: false });
|
||||
|
||||
await buildUser({
|
||||
teamId: user.teamId,
|
||||
isAdmin: false,
|
||||
});
|
||||
const res = await server.post("/api/users.delete", {
|
||||
body: { token: user.getJwtToken(), confirmation: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
confirmation: true,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
|
||||
it("should allow deleting user account with confirmation", async () => {
|
||||
const user = await buildUser();
|
||||
await buildUser({ teamId: user.teamId });
|
||||
|
||||
await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/users.delete", {
|
||||
body: { token: user.getJwtToken(), confirmation: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
confirmation: true,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
@@ -278,16 +321,26 @@ describe("#users.delete", () => {
|
||||
lastActiveAt: null,
|
||||
});
|
||||
const res = await server.post("/api/users.delete", {
|
||||
body: { token: user.getJwtToken(), id: pending.id, confirmation: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: pending.id,
|
||||
confirmation: true,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
|
||||
it("should not allow deleting another user account", async () => {
|
||||
const user = await buildAdmin();
|
||||
const user2 = await buildUser({ teamId: user.teamId });
|
||||
const user2 = await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const res = await server.post("/api/users.delete", {
|
||||
body: { token: user.getJwtToken(), id: user2.id, confirmation: true },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: user2.id,
|
||||
confirmation: true,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
@@ -295,20 +348,20 @@ describe("#users.delete", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/users.delete");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#users.update", () => {
|
||||
it("should update user profile information", async () => {
|
||||
const { user } = await seed();
|
||||
const res = await server.post("/api/users.update", {
|
||||
body: { token: user.getJwtToken(), name: "New name" },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
name: "New name",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.name).toEqual("New name");
|
||||
});
|
||||
@@ -316,21 +369,20 @@ describe("#users.update", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/users.update");
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#users.promote", () => {
|
||||
it("should promote a new admin", async () => {
|
||||
const { admin, user } = await seed();
|
||||
|
||||
const res = await server.post("/api/users.promote", {
|
||||
body: { token: admin.getJwtToken(), id: user.id },
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -338,19 +390,22 @@ describe("#users.promote", () => {
|
||||
it("should require admin", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/users.promote", {
|
||||
body: { token: user.getJwtToken(), id: user.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#users.demote", () => {
|
||||
it("should demote an admin", async () => {
|
||||
const { admin, user } = await seed();
|
||||
await user.update({ isAdmin: true }); // Make another admin
|
||||
await user.update({
|
||||
isAdmin: true,
|
||||
}); // Make another admin
|
||||
|
||||
const res = await server.post("/api/users.demote", {
|
||||
body: {
|
||||
@@ -359,14 +414,15 @@ describe("#users.demote", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should demote an admin to viewer", async () => {
|
||||
const { admin, user } = await seed();
|
||||
await user.update({ isAdmin: true }); // Make another admin
|
||||
await user.update({
|
||||
isAdmin: true,
|
||||
}); // Make another admin
|
||||
|
||||
const res = await server.post("/api/users.demote", {
|
||||
body: {
|
||||
@@ -376,14 +432,15 @@ describe("#users.demote", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should demote an admin to member", async () => {
|
||||
const { admin, user } = await seed();
|
||||
await user.update({ isAdmin: true }); // Make another admin
|
||||
await user.update({
|
||||
isAdmin: true,
|
||||
}); // Make another admin
|
||||
|
||||
const res = await server.post("/api/users.demote", {
|
||||
body: {
|
||||
@@ -393,14 +450,12 @@ describe("#users.demote", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not demote admins if only one available", async () => {
|
||||
const admin = await buildAdmin();
|
||||
|
||||
const res = await server.post("/api/users.demote", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
@@ -408,7 +463,6 @@ describe("#users.demote", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -416,19 +470,19 @@ describe("#users.demote", () => {
|
||||
it("should require admin", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/users.promote", {
|
||||
body: { token: user.getJwtToken(), id: user.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#users.suspend", () => {
|
||||
it("should suspend an user", async () => {
|
||||
const { admin, user } = await seed();
|
||||
|
||||
const res = await server.post("/api/users.suspend", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
@@ -436,7 +490,6 @@ describe("#users.suspend", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -450,7 +503,6 @@ describe("#users.suspend", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -458,15 +510,16 @@ describe("#users.suspend", () => {
|
||||
it("should require admin", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/users.suspend", {
|
||||
body: { token: user.getJwtToken(), id: user.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#users.activate", () => {
|
||||
it("should activate a suspended user", async () => {
|
||||
const { admin, user } = await seed();
|
||||
@@ -474,7 +527,6 @@ describe("#users.activate", () => {
|
||||
suspendedById: admin.id,
|
||||
suspendedAt: new Date(),
|
||||
});
|
||||
|
||||
expect(user.isSuspended).toBe(true);
|
||||
const res = await server.post("/api/users.activate", {
|
||||
body: {
|
||||
@@ -483,7 +535,6 @@ describe("#users.activate", () => {
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -491,24 +542,28 @@ describe("#users.activate", () => {
|
||||
it("should require admin", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/users.activate", {
|
||||
body: { token: user.getJwtToken(), id: user.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#users.count", () => {
|
||||
it("should count active users", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/api/users.count", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.counts.all).toEqual(1);
|
||||
expect(body.data.counts.admins).toEqual(0);
|
||||
@@ -519,12 +574,16 @@ describe("#users.count", () => {
|
||||
|
||||
it("should count admin users", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id, isAdmin: true });
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
isAdmin: true,
|
||||
});
|
||||
const res = await server.post("/api/users.count", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.counts.all).toEqual(1);
|
||||
expect(body.data.counts.admins).toEqual(1);
|
||||
@@ -535,13 +594,19 @@ describe("#users.count", () => {
|
||||
|
||||
it("should count suspended users", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
await buildUser({ teamId: team.id, suspendedAt: new Date() });
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
await buildUser({
|
||||
teamId: team.id,
|
||||
suspendedAt: new Date(),
|
||||
});
|
||||
const res = await server.post("/api/users.count", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.counts.all).toEqual(2);
|
||||
expect(body.data.counts.admins).toEqual(0);
|
||||
@@ -552,12 +617,16 @@ describe("#users.count", () => {
|
||||
|
||||
it("should count invited users", async () => {
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id, lastActiveAt: null });
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
lastActiveAt: null,
|
||||
});
|
||||
const res = await server.post("/api/users.count", {
|
||||
body: { token: user.getJwtToken() },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.counts.all).toEqual(1);
|
||||
expect(body.data.counts.admins).toEqual(0);
|
||||
@@ -1,62 +1,84 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import userDestroyer from "../../commands/userDestroyer";
|
||||
import userInviter from "../../commands/userInviter";
|
||||
import userSuspender from "../../commands/userSuspender";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { Event, User, Team } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { presentUser, presentPolicies } from "../../presenters";
|
||||
import { Op } 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 { presentUser, presentPolicies } from "@server/presenters";
|
||||
import { Op } from "@server/sequelize";
|
||||
import {
|
||||
assertIn,
|
||||
assertSort,
|
||||
assertPresent,
|
||||
assertArray,
|
||||
} from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const { can, authorize } = policy;
|
||||
const router = new Router();
|
||||
|
||||
router.post("users.list", auth(), pagination(), async (ctx) => {
|
||||
let { sort = "createdAt", query, direction, filter } = ctx.body;
|
||||
let { direction } = ctx.body;
|
||||
const { sort = "createdAt", query, filter } = ctx.body;
|
||||
if (direction !== "ASC") direction = "DESC";
|
||||
ctx.assertSort(sort, User);
|
||||
assertSort(sort, User);
|
||||
|
||||
if (filter) {
|
||||
ctx.assertIn(filter, [
|
||||
"invited",
|
||||
"viewers",
|
||||
"admins",
|
||||
"active",
|
||||
"all",
|
||||
"suspended",
|
||||
]);
|
||||
assertIn(
|
||||
filter,
|
||||
["invited", "viewers", "admins", "active", "all", "suspended"],
|
||||
"Invalid filter"
|
||||
);
|
||||
}
|
||||
|
||||
const actor = ctx.state.user;
|
||||
|
||||
let where = {
|
||||
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;
|
||||
}
|
||||
|
||||
case "suspended": {
|
||||
where = { ...where, suspendedAt: { [Op.ne]: null } };
|
||||
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,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case "all": {
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
where = { ...where, suspendedAt: { [Op.eq]: null } };
|
||||
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,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -64,6 +86,7 @@ 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}%`,
|
||||
},
|
||||
@@ -81,14 +104,13 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: {
|
||||
...ctx.state.pagination,
|
||||
total,
|
||||
},
|
||||
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) })
|
||||
presentUser(user, {
|
||||
includeDetails: can(actor, "readDetails", user),
|
||||
})
|
||||
),
|
||||
policies: presentPolicies(actor, users),
|
||||
};
|
||||
@@ -97,7 +119,6 @@ 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,
|
||||
@@ -108,14 +129,13 @@ router.post("users.count", auth(), async (ctx) => {
|
||||
router.post("users.info", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
const actor = ctx.state.user;
|
||||
|
||||
const user = id ? await User.findByPk(id) : actor;
|
||||
authorize(actor, "read", user);
|
||||
|
||||
const includeDetails = can(actor, "readDetails", user);
|
||||
|
||||
ctx.body = {
|
||||
data: presentUser(user, { includeDetails }),
|
||||
data: presentUser(user, {
|
||||
includeDetails,
|
||||
}),
|
||||
policies: presentPolicies(actor, [user]),
|
||||
};
|
||||
});
|
||||
@@ -123,13 +143,10 @@ router.post("users.info", auth(), async (ctx) => {
|
||||
router.post("users.update", auth(), async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
const { name, avatarUrl, language } = ctx.body;
|
||||
|
||||
if (name) user.name = name;
|
||||
if (avatarUrl) user.avatarUrl = avatarUrl;
|
||||
if (language) user.language = language;
|
||||
|
||||
await user.save();
|
||||
|
||||
await Event.create({
|
||||
name: "users.update",
|
||||
actorId: user.id,
|
||||
@@ -137,38 +154,36 @@ router.post("users.update", auth(), async (ctx) => {
|
||||
teamId: user.teamId,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentUser(user, { includeDetails: true }),
|
||||
data: presentUser(user, {
|
||||
includeDetails: true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Admin specific
|
||||
|
||||
router.post("users.promote", auth(), async (ctx) => {
|
||||
const userId = ctx.body.id;
|
||||
const teamId = ctx.state.user.teamId;
|
||||
const actor = ctx.state.user;
|
||||
ctx.assertPresent(userId, "id is required");
|
||||
|
||||
assertPresent(userId, "id is required");
|
||||
const user = await User.findByPk(userId);
|
||||
authorize(actor, "promote", user);
|
||||
|
||||
await user.promote();
|
||||
|
||||
await Event.create({
|
||||
name: "users.promote",
|
||||
actorId: actor.id,
|
||||
userId,
|
||||
teamId,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
const includeDetails = can(actor, "readDetails", user);
|
||||
|
||||
ctx.body = {
|
||||
data: presentUser(user, { includeDetails }),
|
||||
data: presentUser(user, {
|
||||
includeDetails,
|
||||
}),
|
||||
policies: presentPolicies(actor, [user]),
|
||||
};
|
||||
});
|
||||
@@ -177,31 +192,27 @@ router.post("users.demote", auth(), async (ctx) => {
|
||||
const userId = ctx.body.id;
|
||||
const teamId = ctx.state.user.teamId;
|
||||
let { to } = ctx.body;
|
||||
|
||||
const actor = ctx.state.user;
|
||||
ctx.assertPresent(userId, "id is required");
|
||||
|
||||
assertPresent(userId, "id is required");
|
||||
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",
|
||||
actorId: actor.id,
|
||||
userId,
|
||||
teamId,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
const includeDetails = can(actor, "readDetails", user);
|
||||
|
||||
ctx.body = {
|
||||
data: presentUser(user, { includeDetails }),
|
||||
data: presentUser(user, {
|
||||
includeDetails,
|
||||
}),
|
||||
policies: presentPolicies(actor, [user]),
|
||||
};
|
||||
});
|
||||
@@ -209,21 +220,19 @@ router.post("users.demote", auth(), async (ctx) => {
|
||||
router.post("users.suspend", auth(), async (ctx) => {
|
||||
const userId = ctx.body.id;
|
||||
const actor = ctx.state.user;
|
||||
ctx.assertPresent(userId, "id is required");
|
||||
|
||||
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 }),
|
||||
data: presentUser(user, {
|
||||
includeDetails,
|
||||
}),
|
||||
policies: presentPolicies(actor, [user]),
|
||||
};
|
||||
});
|
||||
@@ -232,40 +241,40 @@ router.post("users.activate", auth(), async (ctx) => {
|
||||
const userId = ctx.body.id;
|
||||
const teamId = ctx.state.user.teamId;
|
||||
const actor = ctx.state.user;
|
||||
ctx.assertPresent(userId, "id is required");
|
||||
|
||||
assertPresent(userId, "id is required");
|
||||
const user = await User.findByPk(userId);
|
||||
authorize(actor, "activate", user);
|
||||
|
||||
await user.activate();
|
||||
|
||||
await Event.create({
|
||||
name: "users.activate",
|
||||
actorId: actor.id,
|
||||
userId,
|
||||
teamId,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
const includeDetails = can(actor, "readDetails", user);
|
||||
|
||||
ctx.body = {
|
||||
data: presentUser(user, { includeDetails }),
|
||||
data: presentUser(user, {
|
||||
includeDetails,
|
||||
}),
|
||||
policies: presentPolicies(actor, [user]),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("users.invite", auth(), async (ctx) => {
|
||||
const { invites } = ctx.body;
|
||||
ctx.assertArray(invites, "invites must be an array");
|
||||
|
||||
assertArray(invites, "invites must be an array");
|
||||
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 });
|
||||
|
||||
const response = await userInviter({
|
||||
user,
|
||||
invites,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
ctx.body = {
|
||||
data: {
|
||||
sent: response.sent,
|
||||
@@ -276,22 +285,20 @@ router.post("users.invite", auth(), async (ctx) => {
|
||||
|
||||
router.post("users.delete", auth(), async (ctx) => {
|
||||
const { confirmation, id } = ctx.body;
|
||||
ctx.assertPresent(confirmation, "confirmation is required");
|
||||
|
||||
assertPresent(confirmation, "confirmation is required");
|
||||
const actor = ctx.state.user;
|
||||
let user = actor;
|
||||
|
||||
if (id) {
|
||||
user = await User.findByPk(id);
|
||||
}
|
||||
|
||||
authorize(actor, "delete", user);
|
||||
|
||||
await userDestroyer({
|
||||
user,
|
||||
actor,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
@@ -1,40 +1,42 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
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 "../../models";
|
||||
import { Op } from "../../sequelize";
|
||||
import webService from "../../services/web";
|
||||
import { buildDocument, buildFileOperation } from "../../test/factories";
|
||||
import { flushdb } from "../../test/support";
|
||||
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 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
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({
|
||||
publishedAt: new Date(),
|
||||
});
|
||||
|
||||
const res = await server.post("/api/utils.gc", {
|
||||
body: {
|
||||
token: process.env.UTILS_SECRET,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not destroy documents deleted less than 30 days ago", async () => {
|
||||
@@ -42,15 +44,17 @@ describe("#utils.gc", () => {
|
||||
publishedAt: new Date(),
|
||||
deletedAt: subDays(new Date(), 25),
|
||||
});
|
||||
|
||||
const res = await server.post("/api/utils.gc", {
|
||||
body: {
|
||||
token: process.env.UTILS_SECRET,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("should destroy documents deleted more than 30 days ago", async () => {
|
||||
@@ -58,15 +62,17 @@ describe("#utils.gc", () => {
|
||||
publishedAt: new Date(),
|
||||
deletedAt: subDays(new Date(), 60),
|
||||
});
|
||||
|
||||
const res = await server.post("/api/utils.gc", {
|
||||
body: {
|
||||
token: process.env.UTILS_SECRET,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should destroy draft documents deleted more than 30 days ago", async () => {
|
||||
@@ -74,14 +80,17 @@ describe("#utils.gc", () => {
|
||||
publishedAt: undefined,
|
||||
deletedAt: subDays(new Date(), 60),
|
||||
});
|
||||
|
||||
const res = await server.post("/api/utils.gc", {
|
||||
body: {
|
||||
token: process.env.UTILS_SECRET,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should expire exports older than 30 days ago", async () => {
|
||||
@@ -90,18 +99,15 @@ describe("#utils.gc", () => {
|
||||
state: "complete",
|
||||
createdAt: subDays(new Date(), 30),
|
||||
});
|
||||
|
||||
await buildFileOperation({
|
||||
type: "export",
|
||||
state: "complete",
|
||||
});
|
||||
|
||||
const res = await server.post("/api/utils.gc", {
|
||||
body: {
|
||||
token: process.env.UTILS_SECRET,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await FileOperation.count({
|
||||
where: {
|
||||
type: "export",
|
||||
@@ -110,7 +116,6 @@ describe("#utils.gc", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(data).toEqual(1);
|
||||
});
|
||||
@@ -121,18 +126,15 @@ describe("#utils.gc", () => {
|
||||
state: "complete",
|
||||
createdAt: subDays(new Date(), 29),
|
||||
});
|
||||
|
||||
await buildFileOperation({
|
||||
type: "export",
|
||||
state: "complete",
|
||||
});
|
||||
|
||||
const res = await server.post("/api/utils.gc", {
|
||||
body: {
|
||||
token: process.env.UTILS_SECRET,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await FileOperation.count({
|
||||
where: {
|
||||
type: "export",
|
||||
@@ -141,7 +143,6 @@ describe("#utils.gc", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(data).toEqual(0);
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
import { subDays } from "date-fns";
|
||||
import Router from "koa-router";
|
||||
import documentPermanentDeleter from "../../commands/documentPermanentDeleter";
|
||||
import teamPermanentDeleter from "../../commands/teamPermanentDeleter";
|
||||
import { AuthenticationError } from "../../errors";
|
||||
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";
|
||||
import { Document, Team, FileOperation } from "../../models";
|
||||
import { Op } from "../../sequelize";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@@ -14,14 +13,13 @@ router.post("utils.gc", async (ctx) => {
|
||||
const { token, limit = 500 } = ctx.body;
|
||||
|
||||
if (process.env.UTILS_SECRET !== token) {
|
||||
throw new AuthenticationError("Invalid secret token");
|
||||
throw AuthenticationError("Invalid secret token");
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
"utils",
|
||||
`Permanently destroying upto ${limit} documents older than 30 days…`
|
||||
);
|
||||
|
||||
const documents = await Document.scope("withUnpublished").findAll({
|
||||
attributes: ["id", "teamId", "text", "deletedAt"],
|
||||
where: {
|
||||
@@ -32,16 +30,12 @@ router.post("utils.gc", async (ctx) => {
|
||||
paranoid: false,
|
||||
limit,
|
||||
});
|
||||
|
||||
const countDeletedDocument = await documentPermanentDeleter(documents);
|
||||
|
||||
Logger.info("utils", `Destroyed ${countDeletedDocument} documents`);
|
||||
|
||||
Logger.info(
|
||||
"utils",
|
||||
`Expiring all the collection export older than 30 days…`
|
||||
);
|
||||
|
||||
const exports = await FileOperation.unscoped().findAll({
|
||||
where: {
|
||||
type: "export",
|
||||
@@ -53,18 +47,16 @@ 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();
|
||||
})
|
||||
);
|
||||
|
||||
Logger.info(
|
||||
"utils",
|
||||
`Permanently destroying upto ${limit} teams older than 30 days…`
|
||||
);
|
||||
|
||||
const teams = await Team.findAll({
|
||||
where: {
|
||||
deletedAt: {
|
||||
@@ -1,26 +1,28 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
// @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 { View, CollectionUser } from "../../models";
|
||||
import webService from "../../services/web";
|
||||
import { buildUser } from "../../test/factories";
|
||||
import { flushdb, seed } from "../../test/support";
|
||||
import { View, CollectionUser } from "@server/models";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
|
||||
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({ documentId: document.id, userId: user.id });
|
||||
|
||||
await View.increment({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/views.list", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data[0].count).toBe(1);
|
||||
expect(body.data[0].user.name).toBe(user.name);
|
||||
@@ -30,21 +32,23 @@ describe("#views.list", () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
});
|
||||
|
||||
await View.increment({ documentId: document.id, userId: user.id });
|
||||
|
||||
await View.increment({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
});
|
||||
const res = await server.post("/api/views.list", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data[0].count).toBe(1);
|
||||
expect(body.data[0].user.name).toBe(user.name);
|
||||
@@ -53,10 +57,11 @@ describe("#views.list", () => {
|
||||
it("should require authentication", async () => {
|
||||
const { document } = await seed();
|
||||
const res = await server.post("/api/views.list", {
|
||||
body: { documentId: document.id },
|
||||
body: {
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -65,20 +70,24 @@ describe("#views.list", () => {
|
||||
const { document } = await seed();
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/views.list", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#views.create", () => {
|
||||
it("should allow creating a view record for document", async () => {
|
||||
const { user, document } = await seed();
|
||||
const res = await server.post("/api/views.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.count).toBe(1);
|
||||
});
|
||||
@@ -87,19 +96,19 @@ describe("#views.create", () => {
|
||||
const { user, document, collection } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
|
||||
await CollectionUser.create({
|
||||
createdById: user.id,
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
permission: "read",
|
||||
});
|
||||
|
||||
const res = await server.post("/api/views.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.count).toBe(1);
|
||||
});
|
||||
@@ -107,10 +116,11 @@ describe("#views.create", () => {
|
||||
it("should require authentication", async () => {
|
||||
const { document } = await seed();
|
||||
const res = await server.post("/api/views.create", {
|
||||
body: { documentId: document.id },
|
||||
body: {
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
@@ -119,7 +129,10 @@ describe("#views.create", () => {
|
||||
const { document } = await seed();
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/views.create", {
|
||||
body: { token: user.getJwtToken(), documentId: document.id },
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
@@ -1,23 +1,23 @@
|
||||
// @flow
|
||||
import Router from "koa-router";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import { View, Document, Event } from "../../models";
|
||||
import policy from "../../policies";
|
||||
import { presentView } from "../../presenters";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { View, Document, Event } from "@server/models";
|
||||
import policy 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;
|
||||
ctx.assertUuid(documentId, "documentId is required");
|
||||
assertUuid(documentId, "documentId is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||
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),
|
||||
};
|
||||
@@ -25,24 +25,28 @@ router.post("views.list", auth(), async (ctx) => {
|
||||
|
||||
router.post("views.create", auth(), async (ctx) => {
|
||||
const { documentId } = ctx.body;
|
||||
ctx.assertUuid(documentId, "documentId is required");
|
||||
assertUuid(documentId, "documentId is required");
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
const view = await View.increment({ documentId, userId: user.id });
|
||||
|
||||
const view = await View.increment({
|
||||
documentId,
|
||||
userId: user.id,
|
||||
});
|
||||
await Event.create({
|
||||
name: "views.create",
|
||||
actorId: user.id,
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: user.teamId,
|
||||
data: { title: document.title },
|
||||
data: {
|
||||
title: document.title,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
view.user = user;
|
||||
ctx.body = {
|
||||
data: presentView(view),
|
||||
@@ -1,35 +1,37 @@
|
||||
// @flow
|
||||
// @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 webService from "../../services/web";
|
||||
import { buildUser, buildCollection } from "../../test/factories";
|
||||
import { flushdb } from "../../test/support";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildCollection } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
|
||||
const app = webService();
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe("auth/redirect", () => {
|
||||
it("should redirect to home", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.get(
|
||||
`/auth/redirect?token=${user.getTransferToken()}`,
|
||||
{ redirect: "manual" }
|
||||
{
|
||||
redirect: "manual",
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
expect(res.headers.get("location").endsWith("/home")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should redirect to first collection", async () => {
|
||||
const collection = await buildCollection();
|
||||
const user = await buildUser({ teamId: collection.teamId });
|
||||
const user = await buildUser({
|
||||
teamId: collection.teamId,
|
||||
});
|
||||
const res = await server.get(
|
||||
`/auth/redirect?token=${user.getTransferToken()}`,
|
||||
{ redirect: "manual" }
|
||||
{
|
||||
redirect: "manual",
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(302);
|
||||
expect(res.headers.get("location").endsWith(collection.url)).toBeTruthy();
|
||||
});
|
||||
@@ -1,21 +1,21 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { addMonths } from "date-fns";
|
||||
import Koa from "koa";
|
||||
import bodyParser from "koa-body";
|
||||
import Router from "koa-router";
|
||||
import { AuthenticationError } from "../../errors";
|
||||
import auth from "../../middlewares/authentication";
|
||||
import validation from "../../middlewares/validation";
|
||||
import { Collection, Team, View } from "../../models";
|
||||
import { AuthenticationError } from "@server/errors";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Collection, Team, View } from "@server/models";
|
||||
// @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 "./providers";
|
||||
|
||||
const app = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
router.use(passport.initialize());
|
||||
|
||||
// dynamically load available authentication provider routes
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message
|
||||
providers.forEach((provider) => {
|
||||
if (provider.enabled) {
|
||||
router.use("/", provider.router.routes());
|
||||
@@ -27,27 +27,25 @@ router.get("/redirect", auth(), async (ctx) => {
|
||||
const jwtToken = user.getJwtToken();
|
||||
|
||||
if (jwtToken === ctx.params.token) {
|
||||
throw new AuthenticationError("Cannot extend token");
|
||||
throw AuthenticationError("Cannot extend token");
|
||||
}
|
||||
|
||||
// ensure that the lastActiveAt on user is updated to prevent replay requests
|
||||
await user.updateActiveAt(ctx.request.ip, true);
|
||||
|
||||
ctx.cookies.set("accessToken", jwtToken, {
|
||||
httpOnly: false,
|
||||
expires: addMonths(new Date(), 3),
|
||||
});
|
||||
|
||||
const [team, collection, view] = await Promise.all([
|
||||
Team.findByPk(user.teamId),
|
||||
Collection.findFirstCollectionForUser(user),
|
||||
View.findOne({
|
||||
where: { userId: user.id },
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const hasViewedDocuments = !!view;
|
||||
|
||||
ctx.redirect(
|
||||
!hasViewedDocuments && collection
|
||||
? `${team.url}${collection.url}`
|
||||
@@ -56,7 +54,6 @@ router.get("/redirect", auth(), async (ctx) => {
|
||||
});
|
||||
|
||||
app.use(bodyParser());
|
||||
app.use(validation());
|
||||
app.use(router.routes());
|
||||
|
||||
export default app;
|
||||
@@ -1,20 +1,21 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import { Strategy as AzureStrategy } from "@outlinewiki/passport-azure-ad-oauth2";
|
||||
import jwt from "jsonwebtoken";
|
||||
import Router from "koa-router";
|
||||
import accountProvisioner from "../../../commands/accountProvisioner";
|
||||
import env from "../../../env";
|
||||
import { MicrosoftGraphError } from "../../../errors";
|
||||
import passportMiddleware from "../../../middlewares/passport";
|
||||
import { StateStore, request } from "../../../utils/passport";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import { MicrosoftGraphError } from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { StateStore, request } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "azure";
|
||||
const AZURE_CLIENT_ID = process.env.AZURE_CLIENT_ID;
|
||||
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET;
|
||||
const AZURE_RESOURCE_APP_ID = process.env.AZURE_RESOURCE_APP_ID;
|
||||
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'scopes' implicitly has type 'any[]' in s... Remove this comment to see the full error message
|
||||
const scopes = [];
|
||||
|
||||
export const config = {
|
||||
@@ -32,8 +33,10 @@ if (AZURE_CLIENT_ID) {
|
||||
passReqToCallback: true,
|
||||
resource: AZURE_RESOURCE_APP_ID,
|
||||
store: new StateStore(),
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'scopes' implicitly has an 'any[]' type.
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, params, _, done) {
|
||||
try {
|
||||
// see docs for what the fields in profile represent here:
|
||||
@@ -46,8 +49,9 @@ if (AZURE_CLIENT_ID) {
|
||||
`https://graph.microsoft.com/v1.0/me`,
|
||||
accessToken
|
||||
);
|
||||
|
||||
if (!profileResponse) {
|
||||
throw new MicrosoftGraphError(
|
||||
throw MicrosoftGraphError(
|
||||
"Unable to load user profile from Microsoft Graph API"
|
||||
);
|
||||
}
|
||||
@@ -58,16 +62,19 @@ if (AZURE_CLIENT_ID) {
|
||||
`https://graph.microsoft.com/v1.0/organization`,
|
||||
accessToken
|
||||
);
|
||||
|
||||
if (!organizationResponse) {
|
||||
throw new MicrosoftGraphError(
|
||||
throw MicrosoftGraphError(
|
||||
"Unable to load organization info from Microsoft Graph API"
|
||||
);
|
||||
}
|
||||
|
||||
const organization = organizationResponse.value[0];
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
const email = profile.email || profileResponse.mail;
|
||||
|
||||
if (!email) {
|
||||
throw new MicrosoftGraphError(
|
||||
throw MicrosoftGraphError(
|
||||
"'email' property is required but could not be found in user profile."
|
||||
);
|
||||
}
|
||||
@@ -75,7 +82,6 @@ if (AZURE_CLIENT_ID) {
|
||||
const domain = email.split("@")[1];
|
||||
const subdomain = domain.split(".")[0];
|
||||
const teamName = organization.displayName;
|
||||
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
@@ -84,18 +90,23 @@ if (AZURE_CLIENT_ID) {
|
||||
subdomain,
|
||||
},
|
||||
user: {
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
name: profile.name,
|
||||
email,
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
avatarUrl: profile.picture,
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
providerId: profile.tid,
|
||||
},
|
||||
authentication: {
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
providerId: profile.oid,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'scopes' implicitly has an 'any[]' type.
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
@@ -105,7 +116,6 @@ if (AZURE_CLIENT_ID) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
passport.use(strategy);
|
||||
|
||||
router.get("azure", passport.authenticate(providerName));
|
||||
@@ -1,30 +1,25 @@
|
||||
// @flow
|
||||
// @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 mailer from "../../../mailer";
|
||||
import webService from "../../../services/web";
|
||||
import { buildUser, buildGuestUser, buildTeam } from "../../../test/factories";
|
||||
import { flushdb } from "../../../test/support";
|
||||
import mailer from "@server/mailer";
|
||||
import webService from "@server/services/web";
|
||||
import { buildUser, buildGuestUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
|
||||
const app = webService();
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
jest.mock("../../../mailer");
|
||||
|
||||
beforeEach(async () => {
|
||||
await flushdb();
|
||||
|
||||
// $FlowFixMe – does not understand Jest mocks
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mockReset' does not exist on type '(type... Remove this comment to see the full error message
|
||||
mailer.sendTemplate.mockReset();
|
||||
});
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe("email", () => {
|
||||
it("should require email param", async () => {
|
||||
const res = await server.post("/auth/email", {
|
||||
body: {},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.error).toEqual("validation_error");
|
||||
expect(body.ok).toEqual(false);
|
||||
@@ -32,12 +27,12 @@ describe("email", () => {
|
||||
|
||||
it("should respond with redirect location when user is SSO enabled", async () => {
|
||||
const user = await buildUser();
|
||||
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email: user.email },
|
||||
body: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.redirect).toMatch("slack");
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
@@ -46,19 +41,19 @@ describe("email", () => {
|
||||
it("should respond with redirect location when user is SSO enabled on another subdomain", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
|
||||
const user = await buildUser();
|
||||
|
||||
await buildTeam({
|
||||
subdomain: "example",
|
||||
});
|
||||
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email: user.email },
|
||||
headers: { host: "example.localoutline.com" },
|
||||
body: {
|
||||
email: user.email,
|
||||
},
|
||||
headers: {
|
||||
host: "example.localoutline.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.redirect).toMatch("slack");
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
@@ -66,12 +61,12 @@ describe("email", () => {
|
||||
|
||||
it("should respond with success when user is not SSO enabled", async () => {
|
||||
const user = await buildGuestUser();
|
||||
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email: user.email },
|
||||
body: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
@@ -79,97 +74,116 @@ describe("email", () => {
|
||||
|
||||
it("should respond with success regardless of whether successful to prevent crawling email logins", async () => {
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email: "user@example.com" },
|
||||
body: {
|
||||
email: "user@example.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("with multiple users matching email", () => {
|
||||
it("should default to current subdomain with SSO", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
|
||||
const email = "sso-user@example.org";
|
||||
const team = await buildTeam({
|
||||
subdomain: "example",
|
||||
});
|
||||
|
||||
await buildGuestUser({ email });
|
||||
await buildUser({ email, teamId: team.id });
|
||||
|
||||
await buildGuestUser({
|
||||
email,
|
||||
});
|
||||
await buildUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email },
|
||||
headers: { host: "example.localoutline.com" },
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
headers: {
|
||||
host: "example.localoutline.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.redirect).toMatch("slack");
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to current subdomain with guest email", async () => {
|
||||
process.env.URL = "http://localoutline.com";
|
||||
process.env.SUBDOMAINS_ENABLED = "true";
|
||||
|
||||
const email = "guest-user@example.org";
|
||||
const team = await buildTeam({
|
||||
subdomain: "example",
|
||||
});
|
||||
|
||||
await buildUser({ email });
|
||||
await buildGuestUser({ email, teamId: team.id });
|
||||
|
||||
await buildUser({
|
||||
email,
|
||||
});
|
||||
await buildGuestUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email },
|
||||
headers: { host: "example.localoutline.com" },
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
headers: {
|
||||
host: "example.localoutline.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to custom domain with SSO", async () => {
|
||||
const email = "sso-user-2@example.org";
|
||||
const team = await buildTeam({
|
||||
domain: "docs.mycompany.com",
|
||||
});
|
||||
|
||||
await buildGuestUser({ email });
|
||||
await buildUser({ email, teamId: team.id });
|
||||
|
||||
await buildGuestUser({
|
||||
email,
|
||||
});
|
||||
await buildUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email },
|
||||
headers: { host: "docs.mycompany.com" },
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
headers: {
|
||||
host: "docs.mycompany.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.redirect).toMatch("slack");
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should default to custom domain with guest email", async () => {
|
||||
const email = "guest-user-2@example.org";
|
||||
const team = await buildTeam({
|
||||
domain: "docs.mycompany.com",
|
||||
});
|
||||
|
||||
await buildUser({ email });
|
||||
await buildGuestUser({ email, teamId: team.id });
|
||||
|
||||
await buildUser({
|
||||
email,
|
||||
});
|
||||
await buildGuestUser({
|
||||
email,
|
||||
teamId: team.id,
|
||||
});
|
||||
const res = await server.post("/auth/email", {
|
||||
body: { email },
|
||||
headers: { host: "docs.mycompany.com" },
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
headers: {
|
||||
host: "docs.mycompany.com",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
@@ -1,20 +1,16 @@
|
||||
// @flow
|
||||
import { subMinutes } from "date-fns";
|
||||
import Router from "koa-router";
|
||||
import { find } from "lodash";
|
||||
import {
|
||||
parseDomain,
|
||||
isCustomSubdomain,
|
||||
} from "../../../../shared/utils/domains";
|
||||
import { AuthorizationError } from "../../../errors";
|
||||
import mailer from "../../../mailer";
|
||||
import errorHandling from "../../../middlewares/errorHandling";
|
||||
import methodOverride from "../../../middlewares/methodOverride";
|
||||
import validation from "../../../middlewares/validation";
|
||||
import { User, Team } from "../../../models";
|
||||
import { signIn } from "../../../utils/authentication";
|
||||
import { isCustomDomain } from "../../../utils/domains";
|
||||
import { getUserForEmailSigninToken } from "../../../utils/jwt";
|
||||
import { parseDomain, isCustomSubdomain } from "@shared/utils/domains";
|
||||
import { AuthorizationError } from "@server/errors";
|
||||
import mailer from "@server/mailer";
|
||||
import errorHandling from "@server/middlewares/errorHandling";
|
||||
import methodOverride from "@server/middlewares/methodOverride";
|
||||
import { User, Team } from "@server/models";
|
||||
import { signIn } from "@server/utils/authentication";
|
||||
import { isCustomDomain } from "@server/utils/domains";
|
||||
import { getUserForEmailSigninToken } from "@server/utils/jwt";
|
||||
import { assertEmail, assertPresent } from "@server/validation";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@@ -24,23 +20,25 @@ export const config = {
|
||||
};
|
||||
|
||||
router.use(methodOverride());
|
||||
router.use(validation());
|
||||
|
||||
router.post("email", errorHandling(), async (ctx) => {
|
||||
const { email } = ctx.body;
|
||||
|
||||
ctx.assertEmail(email, "email is required");
|
||||
|
||||
assertEmail(email, "email is required");
|
||||
const users = await User.scope("withAuthentications").findAll({
|
||||
where: { email: email.toLowerCase() },
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (users.length) {
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'team' implicitly has type 'any' in some ... Remove this comment to see the full error message
|
||||
let team;
|
||||
|
||||
if (isCustomDomain(ctx.request.hostname)) {
|
||||
team = await Team.scope("withAuthenticationProviders").findOne({
|
||||
where: { domain: ctx.request.hostname },
|
||||
where: {
|
||||
domain: ctx.request.hostname,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,12 +50,15 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
const domain = parseDomain(ctx.request.hostname);
|
||||
const subdomain = domain ? domain.subdomain : undefined;
|
||||
team = await Team.scope("withAuthenticationProviders").findOne({
|
||||
where: { subdomain },
|
||||
where: {
|
||||
subdomain,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If there are multiple users with this email address then give precedence
|
||||
// to the one that is active on this subdomain/domain (if any)
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
|
||||
let user = users.find((user) => team && user.teamId === team.id);
|
||||
|
||||
// A user was found for the email address, but they don't belong to the team
|
||||
@@ -94,7 +95,7 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
}
|
||||
|
||||
if (!team.guestSignin) {
|
||||
throw new AuthorizationError();
|
||||
throw AuthorizationError();
|
||||
}
|
||||
|
||||
// basic rate limit of endpoint to prevent send email abuse
|
||||
@@ -116,7 +117,6 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
token: user.getEmailSigninToken(),
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
user.lastSigninEmailSentAt = new Date();
|
||||
await user.save();
|
||||
}
|
||||
@@ -129,17 +129,20 @@ router.post("email", errorHandling(), async (ctx) => {
|
||||
|
||||
router.get("email.callback", async (ctx) => {
|
||||
const { token } = ctx.request.query;
|
||||
|
||||
ctx.assertPresent(token, "token is required");
|
||||
assertPresent(token, "token is required");
|
||||
|
||||
try {
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
|
||||
const user = await getUserForEmailSigninToken(token);
|
||||
|
||||
if (!user.team.guestSignin) {
|
||||
return ctx.redirect("/?notice=auth-error");
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
return ctx.redirect("/?notice=suspended");
|
||||
}
|
||||
|
||||
if (user.isInvited) {
|
||||
await mailer.sendTemplate("welcome", {
|
||||
to: user.email,
|
||||
@@ -147,8 +150,9 @@ router.get("email.callback", async (ctx) => {
|
||||
});
|
||||
}
|
||||
|
||||
await user.update({ lastActiveAt: new Date() });
|
||||
|
||||
await user.update({
|
||||
lastActiveAt: new Date(),
|
||||
});
|
||||
// set cookies on response and redirect to team subdomain
|
||||
await signIn(ctx, user, user.team, "email", false, false);
|
||||
} catch (err) {
|
||||
@@ -1,24 +1,24 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import Router from "koa-router";
|
||||
import { capitalize } from "lodash";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'pass... Remove this comment to see the full error message
|
||||
import { Strategy as GoogleStrategy } from "passport-google-oauth2";
|
||||
import accountProvisioner from "../../../commands/accountProvisioner";
|
||||
import env from "../../../env";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import {
|
||||
GoogleWorkspaceRequiredError,
|
||||
GoogleWorkspaceInvalidError,
|
||||
} from "../../../errors";
|
||||
import passportMiddleware from "../../../middlewares/passport";
|
||||
import { getAllowedDomains } from "../../../utils/authentication";
|
||||
import { StateStore } from "../../../utils/passport";
|
||||
} from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { getAllowedDomains } from "@server/utils/authentication";
|
||||
import { StateStore } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "google";
|
||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
||||
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
|
||||
const allowedDomains = getAllowedDomains();
|
||||
|
||||
const scopes = [
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
@@ -40,21 +40,21 @@ if (GOOGLE_CLIENT_ID) {
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
try {
|
||||
const domain = profile._json.hd;
|
||||
|
||||
if (!domain) {
|
||||
throw new GoogleWorkspaceRequiredError();
|
||||
throw GoogleWorkspaceRequiredError();
|
||||
}
|
||||
|
||||
if (allowedDomains.length && !allowedDomains.includes(domain)) {
|
||||
throw new GoogleWorkspaceInvalidError();
|
||||
throw GoogleWorkspaceInvalidError();
|
||||
}
|
||||
|
||||
const subdomain = domain.split(".")[0];
|
||||
const teamName = capitalize(subdomain);
|
||||
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
@@ -1,10 +1,11 @@
|
||||
// @flow
|
||||
import { signin } from "../../../../shared/utils/routeHelpers";
|
||||
import { requireDirectory } from "../../../utils/fs";
|
||||
import { signin } from "@shared/utils/routeHelpers";
|
||||
import { requireDirectory } from "@server/utils/fs";
|
||||
|
||||
let providers = [];
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'providers' implicitly has type 'any[]' i... Remove this comment to see the full error message
|
||||
const providers = [];
|
||||
|
||||
requireDirectory(__dirname).forEach(([module, id]) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'config' does not exist on type 'unknown'... Remove this comment to see the full error message
|
||||
const { config, default: router } = module;
|
||||
|
||||
if (id === "index") {
|
||||
@@ -34,4 +35,5 @@ requireDirectory(__dirname).forEach(([module, id]) => {
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'providers' implicitly has an 'any[]' typ... Remove this comment to see the full error message
|
||||
export default providers;
|
||||
@@ -1,26 +1,26 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import Router from "koa-router";
|
||||
import get from "lodash/get";
|
||||
import { get } from "lodash";
|
||||
import { Strategy } from "passport-oauth2";
|
||||
import accountProvisioner from "../../../commands/accountProvisioner";
|
||||
import env from "../../../env";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import {
|
||||
OIDCMalformedUserInfoError,
|
||||
AuthenticationError,
|
||||
} from "../../../errors";
|
||||
import passportMiddleware from "../../../middlewares/passport";
|
||||
import { getAllowedDomains } from "../../../utils/authentication";
|
||||
import { StateStore, request } from "../../../utils/passport";
|
||||
} from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { getAllowedDomains } from "@server/utils/authentication";
|
||||
import { StateStore, request } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "oidc";
|
||||
const OIDC_DISPLAY_NAME = process.env.OIDC_DISPLAY_NAME || "OpenID Connect";
|
||||
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID;
|
||||
const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET;
|
||||
const OIDC_AUTH_URI = process.env.OIDC_AUTH_URI;
|
||||
const OIDC_TOKEN_URI = process.env.OIDC_TOKEN_URI;
|
||||
const OIDC_USERINFO_URI = process.env.OIDC_USERINFO_URI;
|
||||
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || "";
|
||||
const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || "";
|
||||
const OIDC_AUTH_URI = process.env.OIDC_AUTH_URI || "";
|
||||
const OIDC_TOKEN_URI = process.env.OIDC_TOKEN_URI || "";
|
||||
const OIDC_USERINFO_URI = process.env.OIDC_USERINFO_URI || "";
|
||||
const OIDC_SCOPES = process.env.OIDC_SCOPES || "";
|
||||
const OIDC_USERNAME_CLAIM =
|
||||
process.env.OIDC_USERNAME_CLAIM || "preferred_username";
|
||||
@@ -30,7 +30,6 @@ export const config = {
|
||||
name: OIDC_DISPLAY_NAME,
|
||||
enabled: !!OIDC_CLIENT_ID,
|
||||
};
|
||||
|
||||
const scopes = OIDC_SCOPES.split(" ");
|
||||
|
||||
Strategy.prototype.userProfile = async function (accessToken, done) {
|
||||
@@ -54,40 +53,45 @@ if (OIDC_CLIENT_ID) {
|
||||
callbackURL: `${env.URL}/auth/${providerName}.callback`,
|
||||
passReqToCallback: true,
|
||||
scope: OIDC_SCOPES,
|
||||
// @ts-expect-error custom state store
|
||||
store: new StateStore(),
|
||||
state: true,
|
||||
pkce: false,
|
||||
},
|
||||
|
||||
// OpenID Connect standard profile claims can be found in the official
|
||||
// specification.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
|
||||
// Non-standard claims may be configured by individual identity providers.
|
||||
// Any claim supplied in response to the userinfo request will be
|
||||
// available on the `profile` parameter
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
async function (
|
||||
req: any,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Record<string, string>,
|
||||
done: any
|
||||
) {
|
||||
try {
|
||||
if (!profile.email) {
|
||||
throw new AuthenticationError(
|
||||
throw AuthenticationError(
|
||||
`An email field was not returned in the profile parameter, but is required.`
|
||||
);
|
||||
}
|
||||
|
||||
const parts = profile.email.split("@");
|
||||
const domain = parts.length && parts[1];
|
||||
|
||||
if (!domain) {
|
||||
throw new OIDCMalformedUserInfoError();
|
||||
throw OIDCMalformedUserInfoError();
|
||||
}
|
||||
|
||||
if (allowedDomains.length && !allowedDomains.includes(domain)) {
|
||||
throw new AuthenticationError(
|
||||
throw AuthenticationError(
|
||||
`Domain ${domain} is not on the whitelist`
|
||||
);
|
||||
}
|
||||
|
||||
const subdomain = domain.split(".")[0];
|
||||
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
@@ -115,7 +119,6 @@ if (OIDC_CLIENT_ID) {
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
|
||||
return done(null, result.user, result);
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
@@ -1,203 +0,0 @@
|
||||
// @flow
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import Router from "koa-router";
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
import accountProvisioner from "../../../commands/accountProvisioner";
|
||||
import env from "../../../env";
|
||||
import auth from "../../../middlewares/authentication";
|
||||
import passportMiddleware from "../../../middlewares/passport";
|
||||
import {
|
||||
IntegrationAuthentication,
|
||||
Collection,
|
||||
Integration,
|
||||
Team,
|
||||
} from "../../../models";
|
||||
import { StateStore } from "../../../utils/passport";
|
||||
import * as Slack from "../../../utils/slack";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "slack";
|
||||
const SLACK_CLIENT_ID = process.env.SLACK_KEY;
|
||||
const SLACK_CLIENT_SECRET = process.env.SLACK_SECRET;
|
||||
|
||||
const scopes = [
|
||||
"identity.email",
|
||||
"identity.basic",
|
||||
"identity.avatar",
|
||||
"identity.team",
|
||||
];
|
||||
|
||||
export const config = {
|
||||
name: "Slack",
|
||||
enabled: !!SLACK_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (SLACK_CLIENT_ID) {
|
||||
const strategy = new SlackStrategy(
|
||||
{
|
||||
clientID: SLACK_CLIENT_ID,
|
||||
clientSecret: SLACK_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/slack.callback`,
|
||||
passReqToCallback: true,
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
try {
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
name: profile.team.name,
|
||||
subdomain: profile.team.domain,
|
||||
avatarUrl: profile.team.image_230,
|
||||
},
|
||||
user: {
|
||||
name: profile.user.name,
|
||||
email: profile.user.email,
|
||||
avatarUrl: profile.user.image_192,
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
providerId: profile.team.id,
|
||||
},
|
||||
authentication: {
|
||||
providerId: profile.user.id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
return done(null, result.user, result);
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// For some reason the author made the strategy name capatilised, I don't know
|
||||
// why but we need everything lowercase so we just monkey-patch it here.
|
||||
strategy.name = providerName;
|
||||
passport.use(strategy);
|
||||
|
||||
router.get("slack", passport.authenticate(providerName));
|
||||
|
||||
router.get("slack.callback", passportMiddleware(providerName));
|
||||
|
||||
router.get("slack.commands", auth({ required: false }), async (ctx) => {
|
||||
const { code, state, error } = ctx.request.query;
|
||||
const user = ctx.state.user;
|
||||
ctx.assertPresent(code || error, "code is required");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the appropriate
|
||||
// subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
if (state) {
|
||||
try {
|
||||
const team = await Team.findByPk(state);
|
||||
return ctx.redirect(
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.commands`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "command",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
settings: {
|
||||
serviceTeamId: data.team_id,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
});
|
||||
|
||||
router.get("slack.post", auth({ required: false }), async (ctx) => {
|
||||
const { code, error, state } = ctx.request.query;
|
||||
const user = ctx.state.user;
|
||||
ctx.assertPresent(code || error, "code is required");
|
||||
|
||||
const collectionId = state;
|
||||
ctx.assertUuid(collectionId, "collectionId must be an uuid");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the
|
||||
// appropriate subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
try {
|
||||
const collection = await Collection.findByPk(state);
|
||||
const team = await Team.findByPk(collection.teamId);
|
||||
return ctx.redirect(
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.post`;
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "post",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
collectionId,
|
||||
events: [],
|
||||
settings: {
|
||||
url: data.incoming_webhook.url,
|
||||
channel: data.incoming_webhook.channel,
|
||||
channelId: data.incoming_webhook.channel_id,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
});
|
||||
}
|
||||
|
||||
export default router;
|
||||
211
server/routes/auth/providers/slack.ts
Normal file
211
server/routes/auth/providers/slack.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import Router from "koa-router";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'pass... Remove this comment to see the full error message
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import {
|
||||
IntegrationAuthentication,
|
||||
Collection,
|
||||
Integration,
|
||||
Team,
|
||||
} from "@server/models";
|
||||
import { StateStore } from "@server/utils/passport";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import { assertPresent, assertUuid } from "@server/validation";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "slack";
|
||||
const SLACK_CLIENT_ID = process.env.SLACK_KEY;
|
||||
const SLACK_CLIENT_SECRET = process.env.SLACK_SECRET;
|
||||
const scopes = [
|
||||
"identity.email",
|
||||
"identity.basic",
|
||||
"identity.avatar",
|
||||
"identity.team",
|
||||
];
|
||||
|
||||
export const config = {
|
||||
name: "Slack",
|
||||
enabled: !!SLACK_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (SLACK_CLIENT_ID) {
|
||||
const strategy = new SlackStrategy(
|
||||
{
|
||||
clientID: SLACK_CLIENT_ID,
|
||||
clientSecret: SLACK_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/slack.callback`,
|
||||
passReqToCallback: true,
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
try {
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
name: profile.team.name,
|
||||
subdomain: profile.team.domain,
|
||||
avatarUrl: profile.team.image_230,
|
||||
},
|
||||
user: {
|
||||
name: profile.user.name,
|
||||
email: profile.user.email,
|
||||
avatarUrl: profile.user.image_192,
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
providerId: profile.team.id,
|
||||
},
|
||||
authentication: {
|
||||
providerId: profile.user.id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
return done(null, result.user, result);
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
// For some reason the author made the strategy name capatilised, I don't know
|
||||
// why but we need everything lowercase so we just monkey-patch it here.
|
||||
strategy.name = providerName;
|
||||
passport.use(strategy);
|
||||
|
||||
router.get("slack", passport.authenticate(providerName));
|
||||
|
||||
router.get("slack.callback", passportMiddleware(providerName));
|
||||
|
||||
router.get(
|
||||
"slack.commands",
|
||||
auth({
|
||||
required: false,
|
||||
}),
|
||||
async (ctx) => {
|
||||
const { code, state, error } = ctx.request.query;
|
||||
const user = ctx.state.user;
|
||||
assertPresent(code || error, "code is required");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the appropriate
|
||||
// subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
if (state) {
|
||||
try {
|
||||
const team = await Team.findByPk(state);
|
||||
return ctx.redirect(
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.commands`;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "command",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
settings: {
|
||||
serviceTeamId: data.team_id,
|
||||
},
|
||||
});
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
"slack.post",
|
||||
auth({
|
||||
required: false,
|
||||
}),
|
||||
async (ctx) => {
|
||||
const { code, error, state } = ctx.request.query;
|
||||
const user = ctx.state.user;
|
||||
assertPresent(code || error, "code is required");
|
||||
const collectionId = state;
|
||||
assertUuid(collectionId, "collectionId must be an uuid");
|
||||
|
||||
if (error) {
|
||||
ctx.redirect(`/settings/integrations/slack?error=${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the
|
||||
// appropriate subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
try {
|
||||
const collection = await Collection.findByPk(state);
|
||||
const team = await Team.findByPk(collection.teamId);
|
||||
return ctx.redirect(
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
`/settings/integrations/slack?error=unauthenticated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `${process.env.URL || ""}/auth/slack.post`;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
|
||||
const data = await Slack.oauthAccess(code, endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "post",
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
collectionId,
|
||||
events: [],
|
||||
settings: {
|
||||
url: data.incoming_webhook.url,
|
||||
channel: data.incoming_webhook.channel,
|
||||
channelId: data.incoming_webhook.channel_id,
|
||||
},
|
||||
});
|
||||
ctx.redirect("/settings/integrations/slack");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -1,22 +1,20 @@
|
||||
// @flow
|
||||
// @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 { buildShare, buildDocument } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import webService from "../services/web";
|
||||
import { buildShare, buildDocument } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
|
||||
const app = webService();
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe("/share/:id", () => {
|
||||
it("should return standard title in html when loading share", async () => {
|
||||
const share = await buildShare({ published: false });
|
||||
|
||||
const share = await buildShare({
|
||||
published: false,
|
||||
});
|
||||
const res = await server.get(`/share/${share.id}`);
|
||||
const body = await res.text();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toContain("<title>Outline</title>");
|
||||
});
|
||||
@@ -24,42 +22,40 @@ describe("/share/:id", () => {
|
||||
it("should return standard title in html when share does not exist", async () => {
|
||||
const res = await server.get(`/share/junk`);
|
||||
const body = await res.text();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toContain("<title>Outline</title>");
|
||||
});
|
||||
|
||||
it("should return standard title in html when document is deleted", async () => {
|
||||
const document = await buildDocument();
|
||||
const share = await buildShare({ documentId: document.id });
|
||||
|
||||
const share = await buildShare({
|
||||
documentId: document.id,
|
||||
});
|
||||
await document.destroy();
|
||||
|
||||
const res = await server.get(`/share/${share.id}`);
|
||||
const body = await res.text();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toContain("<title>Outline</title>");
|
||||
});
|
||||
|
||||
it("should return document title in html when loading published share", async () => {
|
||||
const document = await buildDocument();
|
||||
const share = await buildShare({ documentId: document.id });
|
||||
|
||||
const share = await buildShare({
|
||||
documentId: document.id,
|
||||
});
|
||||
const res = await server.get(`/share/${share.id}`);
|
||||
const body = await res.text();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toContain(`<title>${document.title}</title>`);
|
||||
});
|
||||
|
||||
it("should return document title in html when loading published share with nested doc route", async () => {
|
||||
const document = await buildDocument();
|
||||
const share = await buildShare({ documentId: document.id });
|
||||
|
||||
const share = await buildShare({
|
||||
documentId: document.id,
|
||||
});
|
||||
const res = await server.get(`/share/${share.id}/doc/test-Cl6g1AgPYn`);
|
||||
const body = await res.text();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body).toContain(`<title>${document.title}</title>`);
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
@@ -7,14 +6,14 @@ import Router from "koa-router";
|
||||
import send from "koa-send";
|
||||
import serve from "koa-static";
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import { languages } from "../../shared/i18n";
|
||||
import env from "../env";
|
||||
import { languages } from "@shared/i18n";
|
||||
import env from "@server/env";
|
||||
import Share from "@server/models/Share";
|
||||
import { opensearchResponse } from "@server/utils/opensearch";
|
||||
import prefetchTags from "@server/utils/prefetchTags";
|
||||
import { robotsResponse } from "@server/utils/robots";
|
||||
import apexRedirect from "../middlewares/apexRedirect";
|
||||
import Share from "../models/Share";
|
||||
import presentEnv from "../presenters/env";
|
||||
import { opensearchResponse } from "../utils/opensearch";
|
||||
import prefetchTags from "../utils/prefetchTags";
|
||||
import { robotsResponse } from "../utils/robots";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const isTest = process.env.NODE_ENV === "test";
|
||||
@@ -22,30 +21,34 @@ const koa = new Koa();
|
||||
const router = new Router();
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'ctx' implicitly has an 'any' type.
|
||||
const readIndexFile = async (ctx) => {
|
||||
if (isProduction) {
|
||||
return readFile(path.join(__dirname, "../../app/index.html"));
|
||||
}
|
||||
|
||||
if (isTest) {
|
||||
return readFile(path.join(__dirname, "../static/index.html"));
|
||||
}
|
||||
|
||||
const middleware = ctx.devMiddleware;
|
||||
await new Promise((resolve) => middleware.waitUntilValid(resolve));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
middleware.fileSystem.readFile(
|
||||
`${ctx.webpackConfig.output.path}/index.html`,
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'err' implicitly has an 'any' type.
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'ctx' implicitly has an 'any' type.
|
||||
const renderApp = async (ctx, next, title = "Outline") => {
|
||||
if (ctx.request.path === "/realtime/") {
|
||||
return next();
|
||||
@@ -55,6 +58,7 @@ const renderApp = async (ctx, next, title = "Outline") => {
|
||||
const environment = `
|
||||
window.env = ${JSON.stringify(presentEnv(env))};
|
||||
`;
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
ctx.body = page
|
||||
.toString()
|
||||
.replace(/\/\/inject-env\/\//g, environment)
|
||||
@@ -63,9 +67,9 @@ const renderApp = async (ctx, next, title = "Outline") => {
|
||||
.replace(/\/\/inject-slack-app-id\/\//g, process.env.SLACK_APP_ID || "");
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'ctx' implicitly has an 'any' type.
|
||||
const renderShare = async (ctx, next) => {
|
||||
const { shareId } = ctx.params;
|
||||
|
||||
// Find the share record if publicly published so that the document title
|
||||
// can be be returned in the server-rendered HTML. This allows it to appear in
|
||||
// unfurls with more reliablity
|
||||
@@ -82,7 +86,6 @@ const renderShare = async (ctx, next) => {
|
||||
|
||||
// Allow shares to be embedded in iframes on other websites
|
||||
ctx.remove("X-Frame-Options");
|
||||
|
||||
return renderApp(ctx, next, share?.document?.title);
|
||||
};
|
||||
|
||||
@@ -98,7 +101,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
try {
|
||||
await send(ctx, ctx.path.substring(8), {
|
||||
root: path.join(__dirname, "../../app/"),
|
||||
setHeaders: (res, path, stat) => {
|
||||
setHeaders: (res) => {
|
||||
res.setHeader("Service-Worker-Allowed", "/");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Cache-Control", `max-age=${365 * 24 * 60 * 60}`);
|
||||
@@ -119,7 +122,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
router.get("/locales/:lng.json", async (ctx) => {
|
||||
let { lng } = ctx.params;
|
||||
const { lng } = ctx.params;
|
||||
|
||||
if (!languages.includes(lng)) {
|
||||
ctx.status = 404;
|
||||
@@ -127,7 +130,7 @@ router.get("/locales/:lng.json", async (ctx) => {
|
||||
}
|
||||
|
||||
await send(ctx, path.join(lng, "translation.json"), {
|
||||
setHeaders: (res, path, stat) => {
|
||||
setHeaders: (res) => {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
res.setHeader("Cache-Control", `max-age=${7 * 24 * 60 * 60}`);
|
||||
}
|
||||
@@ -137,7 +140,7 @@ router.get("/locales/:lng.json", async (ctx) => {
|
||||
});
|
||||
|
||||
router.get("/robots.txt", (ctx) => {
|
||||
ctx.body = robotsResponse(ctx);
|
||||
ctx.body = robotsResponse();
|
||||
});
|
||||
|
||||
router.get("/opensearch.xml", (ctx) => {
|
||||
@@ -146,6 +149,7 @@ router.get("/opensearch.xml", (ctx) => {
|
||||
});
|
||||
|
||||
router.get("/share/:shareId", renderShare);
|
||||
|
||||
router.get("/share/:shareId/*", renderShare);
|
||||
|
||||
// catch all for application
|
||||
@@ -155,6 +159,7 @@ router.get("*", renderApp);
|
||||
// must be provided when serving the application, see:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin
|
||||
const timingOrigins = [env.URL];
|
||||
|
||||
if (env.SENTRY_DSN) {
|
||||
timingOrigins.push("https://sentry.io");
|
||||
}
|
||||
@@ -163,7 +168,6 @@ koa.use(async (ctx, next) => {
|
||||
ctx.set("Timing-Allow-Origin", timingOrigins.join(", "));
|
||||
await next();
|
||||
});
|
||||
|
||||
koa.use(apexRedirect());
|
||||
koa.use(router.routes());
|
||||
|
||||
Reference in New Issue
Block a user