feat: Adds route-level role filtering. (#3734)
* feat: Adds route-level role filtering. Another layer in the onion of security and performance * fix: Regression in authentication middleware
This commit is contained in:
@@ -8,7 +8,7 @@ import pagination from "./middlewares/pagination";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post("apiKeys.create", auth(), async (ctx) => {
|
||||
router.post("apiKeys.create", auth({ member: true }), async (ctx) => {
|
||||
const { name } = ctx.body;
|
||||
assertPresent(name, "name is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -35,24 +35,29 @@ router.post("apiKeys.create", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("apiKeys.list", auth(), pagination(), async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
const keys = await ApiKey.findAll({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
router.post(
|
||||
"apiKeys.list",
|
||||
auth({ member: true }),
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
const keys = await ApiKey.findAll({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: keys.map(presentApiKey),
|
||||
};
|
||||
});
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: keys.map(presentApiKey),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
router.post("apiKeys.delete", auth(), async (ctx) => {
|
||||
router.post("apiKeys.delete", auth({ member: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
buildCollection,
|
||||
buildUser,
|
||||
buildDocument,
|
||||
buildViewer,
|
||||
} from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
|
||||
@@ -1432,12 +1433,76 @@ describe("#documents.archived", () => {
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should require member", async () => {
|
||||
const viewer = await buildViewer();
|
||||
const res = await server.post("/api/documents.archived", {
|
||||
body: {
|
||||
token: viewer.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/documents.archived");
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#documents.deleted", () => {
|
||||
it("should return deleted documents", async () => {
|
||||
const { user } = await seed();
|
||||
const document = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await document.delete(user.id);
|
||||
const res = await server.post("/api/documents.deleted", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not return documents in private collections not a member of", async () => {
|
||||
const { user } = await seed();
|
||||
const collection = await buildCollection({
|
||||
permission: null,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
teamId: user.teamId,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
await document.delete(user.id);
|
||||
const res = await server.post("/api/documents.deleted", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should require member", async () => {
|
||||
const viewer = await buildViewer();
|
||||
const res = await server.post("/api/documents.deleted", {
|
||||
body: {
|
||||
token: viewer.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/documents.deleted");
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#documents.viewed", () => {
|
||||
it("should return empty result if no views", async () => {
|
||||
const { user } = await seed();
|
||||
|
||||
@@ -162,104 +162,117 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.archived", auth(), pagination(), async (ctx) => {
|
||||
const { sort = "updatedAt" } = ctx.body;
|
||||
router.post(
|
||||
"documents.archived",
|
||||
auth({ member: true }),
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
const { sort = "updatedAt" } = ctx.body;
|
||||
|
||||
assertSort(sort, Document);
|
||||
let direction = ctx.body.direction;
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
assertSort(sort, Document);
|
||||
let direction = ctx.body.direction;
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
const collectionIds = await user.collectionIds();
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
};
|
||||
const viewScope: Readonly<ScopeOptions> = {
|
||||
method: ["withViews", user.id],
|
||||
};
|
||||
const documents = await Document.scope([
|
||||
"defaultScope",
|
||||
collectionScope,
|
||||
viewScope,
|
||||
]).findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
collectionId: collectionIds,
|
||||
archivedAt: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
documents.map((document) => presentDocument(document))
|
||||
);
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
const collectionIds = await user.collectionIds();
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
};
|
||||
const viewScope: Readonly<ScopeOptions> = {
|
||||
method: ["withViews", user.id],
|
||||
};
|
||||
const documents = await Document.scope([
|
||||
"defaultScope",
|
||||
collectionScope,
|
||||
viewScope,
|
||||
]).findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
collectionId: collectionIds,
|
||||
archivedAt: {
|
||||
[Op.ne]: null,
|
||||
);
|
||||
|
||||
router.post(
|
||||
"documents.deleted",
|
||||
auth({ member: true }),
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
const { sort = "deletedAt" } = ctx.body;
|
||||
|
||||
assertSort(sort, Document);
|
||||
let direction = ctx.body.direction;
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
const collectionIds = await user.collectionIds({
|
||||
paranoid: false,
|
||||
});
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
};
|
||||
const viewScope: Readonly<ScopeOptions> = {
|
||||
method: ["withViews", user.id],
|
||||
};
|
||||
const documents = await Document.scope([
|
||||
collectionScope,
|
||||
viewScope,
|
||||
]).findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
collectionId: collectionIds,
|
||||
deletedAt: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
documents.map((document) => presentDocument(document))
|
||||
);
|
||||
const policies = presentPolicies(user, documents);
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "createdBy",
|
||||
paranoid: false,
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: "updatedBy",
|
||||
paranoid: false,
|
||||
},
|
||||
],
|
||||
paranoid: false,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
documents.map((document) => presentDocument(document))
|
||||
);
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.deleted", auth(), pagination(), async (ctx) => {
|
||||
const { sort = "deletedAt" } = ctx.body;
|
||||
|
||||
assertSort(sort, Document);
|
||||
let direction = ctx.body.direction;
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
const collectionIds = await user.collectionIds({
|
||||
paranoid: false,
|
||||
});
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
};
|
||||
const viewScope: Readonly<ScopeOptions> = {
|
||||
method: ["withViews", user.id],
|
||||
};
|
||||
const documents = await Document.scope([collectionScope, viewScope]).findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
collectionId: collectionIds,
|
||||
deletedAt: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "createdBy",
|
||||
paranoid: false,
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: "updatedBy",
|
||||
paranoid: false,
|
||||
},
|
||||
],
|
||||
paranoid: false,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
documents.map((document) => presentDocument(document))
|
||||
);
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
);
|
||||
|
||||
router.post("documents.viewed", auth(), pagination(), async (ctx) => {
|
||||
let { direction } = ctx.body;
|
||||
@@ -314,76 +327,81 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.drafts", auth(), pagination(), async (ctx) => {
|
||||
let { direction } = ctx.body;
|
||||
const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;
|
||||
router.post(
|
||||
"documents.drafts",
|
||||
auth({ member: true }),
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
let { direction } = ctx.body;
|
||||
const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;
|
||||
|
||||
assertSort(sort, Document);
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
assertSort(sort, Document);
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
|
||||
if (collectionId) {
|
||||
assertUuid(collectionId, "collectionId must be a UUID");
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "read", collection);
|
||||
}
|
||||
if (collectionId) {
|
||||
assertUuid(collectionId, "collectionId must be a UUID");
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "read", collection);
|
||||
}
|
||||
|
||||
const collectionIds = collectionId
|
||||
? [collectionId]
|
||||
: await user.collectionIds();
|
||||
const where: WhereOptions<Document> = {
|
||||
createdById: user.id,
|
||||
collectionId: collectionIds,
|
||||
publishedAt: {
|
||||
[Op.is]: null,
|
||||
},
|
||||
};
|
||||
|
||||
if (dateFilter) {
|
||||
assertIn(
|
||||
dateFilter,
|
||||
["day", "week", "month", "year"],
|
||||
"dateFilter must be one of day,week,month,year"
|
||||
);
|
||||
where.updatedAt = {
|
||||
[Op.gte]: subtractDate(new Date(), dateFilter),
|
||||
const collectionIds = collectionId
|
||||
? [collectionId]
|
||||
: await user.collectionIds();
|
||||
const where: WhereOptions<Document> = {
|
||||
createdById: user.id,
|
||||
collectionId: collectionIds,
|
||||
publishedAt: {
|
||||
[Op.is]: null,
|
||||
},
|
||||
};
|
||||
|
||||
if (dateFilter) {
|
||||
assertIn(
|
||||
dateFilter,
|
||||
["day", "week", "month", "year"],
|
||||
"dateFilter must be one of day,week,month,year"
|
||||
);
|
||||
where.updatedAt = {
|
||||
[Op.gte]: subtractDate(new Date(), dateFilter),
|
||||
};
|
||||
} else {
|
||||
delete where.updatedAt;
|
||||
}
|
||||
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
};
|
||||
const documents = await Document.scope([
|
||||
"defaultScope",
|
||||
collectionScope,
|
||||
]).findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
documents.map((document) => presentDocument(document))
|
||||
);
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
} else {
|
||||
delete where.updatedAt;
|
||||
}
|
||||
|
||||
const collectionScope: Readonly<ScopeOptions> = {
|
||||
method: ["withCollectionPermissions", user.id],
|
||||
};
|
||||
const documents = await Document.scope([
|
||||
"defaultScope",
|
||||
collectionScope,
|
||||
]).findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
documents.map((document) => presentDocument(document))
|
||||
);
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
);
|
||||
|
||||
router.post(
|
||||
"documents.info",
|
||||
auth({
|
||||
required: false,
|
||||
optional: true,
|
||||
}),
|
||||
async (ctx) => {
|
||||
const { id, shareId, apiVersion } = ctx.body;
|
||||
@@ -421,7 +439,7 @@ router.post(
|
||||
router.post(
|
||||
"documents.export",
|
||||
auth({
|
||||
required: false,
|
||||
optional: true,
|
||||
}),
|
||||
async (ctx) => {
|
||||
const { id, shareId } = ctx.body;
|
||||
@@ -438,7 +456,7 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
router.post("documents.restore", auth(), async (ctx) => {
|
||||
router.post("documents.restore", auth({ member: true }), async (ctx) => {
|
||||
const { id, collectionId, revisionId } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -588,7 +606,7 @@ router.post("documents.search_titles", auth(), pagination(), async (ctx) => {
|
||||
router.post(
|
||||
"documents.search",
|
||||
auth({
|
||||
required: false,
|
||||
optional: true,
|
||||
}),
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
@@ -782,7 +800,7 @@ router.post("documents.unstar", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.templatize", auth(), async (ctx) => {
|
||||
router.post("documents.templatize", auth({ member: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -829,7 +847,7 @@ router.post("documents.templatize", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.update", auth(), async (ctx) => {
|
||||
router.post("documents.update", auth({ member: true }), async (ctx) => {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
@@ -889,7 +907,7 @@ router.post("documents.update", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.move", auth(), async (ctx) => {
|
||||
router.post("documents.move", auth({ member: true }), async (ctx) => {
|
||||
const { id, collectionId, parentDocumentId, index } = ctx.body;
|
||||
assertUuid(id, "id must be a uuid");
|
||||
assertUuid(collectionId, "collectionId must be a uuid");
|
||||
@@ -955,7 +973,7 @@ router.post("documents.move", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.archive", auth(), async (ctx) => {
|
||||
router.post("documents.archive", auth({ member: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -984,7 +1002,7 @@ router.post("documents.archive", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.delete", auth(), async (ctx) => {
|
||||
router.post("documents.delete", auth({ member: true }), async (ctx) => {
|
||||
const { id, permanent } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -1045,7 +1063,7 @@ router.post("documents.delete", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.unpublish", auth(), async (ctx) => {
|
||||
router.post("documents.unpublish", auth({ member: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -1079,7 +1097,7 @@ router.post("documents.unpublish", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.import", auth(), async (ctx) => {
|
||||
router.post("documents.import", auth({ member: true }), async (ctx) => {
|
||||
const { publish, collectionId, parentDocumentId, index } = ctx.body;
|
||||
|
||||
if (!ctx.is("multipart/form-data")) {
|
||||
@@ -1162,7 +1180,7 @@ router.post("documents.import", auth(), async (ctx) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.post("documents.create", auth(), async (ctx) => {
|
||||
router.post("documents.create", auth({ member: true }), async (ctx) => {
|
||||
const {
|
||||
title = "",
|
||||
text = "",
|
||||
|
||||
@@ -13,7 +13,7 @@ import pagination from "./middlewares/pagination";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post("fileOperations.info", auth(), async (ctx) => {
|
||||
router.post("fileOperations.info", auth({ admin: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
@@ -28,42 +28,47 @@ router.post("fileOperations.info", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("fileOperations.list", auth(), pagination(), async (ctx) => {
|
||||
let { direction } = ctx.body;
|
||||
const { sort = "createdAt", type } = ctx.body;
|
||||
assertIn(type, Object.values(FileOperationType));
|
||||
assertSort(sort, FileOperation);
|
||||
router.post(
|
||||
"fileOperations.list",
|
||||
auth({ admin: true }),
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
let { direction } = ctx.body;
|
||||
const { sort = "createdAt", type } = ctx.body;
|
||||
assertIn(type, Object.values(FileOperationType));
|
||||
assertSort(sort, FileOperation);
|
||||
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
const where: WhereOptions<FileOperation> = {
|
||||
teamId: user.teamId,
|
||||
type,
|
||||
};
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
authorize(user, "manage", team);
|
||||
|
||||
const [exports, total] = await Promise.all([
|
||||
await FileOperation.findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
}),
|
||||
await FileOperation.count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: { ...ctx.state.pagination, total },
|
||||
data: exports.map(presentFileOperation),
|
||||
};
|
||||
}
|
||||
const { user } = ctx.state;
|
||||
const where: WhereOptions<FileOperation> = {
|
||||
teamId: user.teamId,
|
||||
type,
|
||||
};
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
authorize(user, "manage", team);
|
||||
);
|
||||
|
||||
const [exports, total] = await Promise.all([
|
||||
await FileOperation.findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
}),
|
||||
await FileOperation.count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: { ...ctx.state.pagination, total },
|
||||
data: exports.map(presentFileOperation),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("fileOperations.redirect", auth(), async (ctx) => {
|
||||
router.post("fileOperations.redirect", auth({ admin: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
@@ -81,7 +86,7 @@ router.post("fileOperations.redirect", auth(), async (ctx) => {
|
||||
ctx.redirect(accessUrl);
|
||||
});
|
||||
|
||||
router.post("fileOperations.delete", auth(), async (ctx) => {
|
||||
router.post("fileOperations.delete", auth({ admin: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
|
||||
@@ -11,127 +11,144 @@ import pagination from "./middlewares/pagination";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post("webhookSubscriptions.list", auth(), pagination(), async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "listWebhookSubscription", user.team);
|
||||
const webhooks = await WebhookSubscription.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
},
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
router.post(
|
||||
"webhookSubscriptions.list",
|
||||
auth({ admin: true }),
|
||||
pagination(),
|
||||
async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "listWebhookSubscription", user.team);
|
||||
const webhooks = await WebhookSubscription.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
},
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: webhooks.map(presentWebhookSubscription),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("webhookSubscriptions.create", auth(), async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "createWebhookSubscription", user.team);
|
||||
|
||||
const { name, url } = ctx.request.body;
|
||||
const events: string[] = compact(ctx.request.body.events);
|
||||
assertPresent(name, "name is required");
|
||||
assertPresent(url, "url is required");
|
||||
assertArray(events, "events is required");
|
||||
if (events.length === 0) {
|
||||
throw ValidationError("events are required");
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: webhooks.map(presentWebhookSubscription),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const webhookSubscription = await WebhookSubscription.create({
|
||||
name,
|
||||
events,
|
||||
createdById: user.id,
|
||||
teamId: user.teamId,
|
||||
url,
|
||||
enabled: true,
|
||||
});
|
||||
router.post(
|
||||
"webhookSubscriptions.create",
|
||||
auth({ admin: true }),
|
||||
async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "createWebhookSubscription", user.team);
|
||||
|
||||
const event: WebhookSubscriptionEvent = {
|
||||
name: "webhook_subscriptions.create",
|
||||
modelId: webhookSubscription.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: {
|
||||
const { name, url } = ctx.request.body;
|
||||
const events: string[] = compact(ctx.request.body.events);
|
||||
assertPresent(name, "name is required");
|
||||
assertPresent(url, "url is required");
|
||||
assertArray(events, "events is required");
|
||||
if (events.length === 0) {
|
||||
throw ValidationError("events are required");
|
||||
}
|
||||
|
||||
const webhookSubscription = await WebhookSubscription.create({
|
||||
name,
|
||||
url,
|
||||
events,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
};
|
||||
await Event.create(event);
|
||||
createdById: user.id,
|
||||
teamId: user.teamId,
|
||||
url,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentWebhookSubscription(webhookSubscription),
|
||||
};
|
||||
});
|
||||
const event: WebhookSubscriptionEvent = {
|
||||
name: "webhook_subscriptions.create",
|
||||
modelId: webhookSubscription.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: {
|
||||
name,
|
||||
url,
|
||||
events,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
};
|
||||
await Event.create(event);
|
||||
|
||||
router.post("webhookSubscriptions.delete", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
const webhookSubscription = await WebhookSubscription.findByPk(id);
|
||||
|
||||
authorize(user, "delete", webhookSubscription);
|
||||
|
||||
await webhookSubscription.destroy();
|
||||
|
||||
const event: WebhookSubscriptionEvent = {
|
||||
name: "webhook_subscriptions.delete",
|
||||
modelId: webhookSubscription.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: {
|
||||
name: webhookSubscription.name,
|
||||
url: webhookSubscription.url,
|
||||
events: webhookSubscription.events,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
};
|
||||
await Event.create(event);
|
||||
});
|
||||
|
||||
router.post("webhookSubscriptions.update", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
|
||||
const { name, url } = ctx.request.body;
|
||||
const events: string[] = compact(ctx.request.body.events);
|
||||
assertPresent(name, "name is required");
|
||||
assertPresent(url, "url is required");
|
||||
assertArray(events, "events is required");
|
||||
if (events.length === 0) {
|
||||
throw ValidationError("events are required");
|
||||
ctx.body = {
|
||||
data: presentWebhookSubscription(webhookSubscription),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const webhookSubscription = await WebhookSubscription.findByPk(id);
|
||||
router.post(
|
||||
"webhookSubscriptions.delete",
|
||||
auth({ admin: true }),
|
||||
async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
const webhookSubscription = await WebhookSubscription.findByPk(id);
|
||||
|
||||
authorize(user, "update", webhookSubscription);
|
||||
authorize(user, "delete", webhookSubscription);
|
||||
|
||||
await webhookSubscription.update({ name, url, events, enabled: true });
|
||||
await webhookSubscription.destroy();
|
||||
|
||||
const event: WebhookSubscriptionEvent = {
|
||||
name: "webhook_subscriptions.update",
|
||||
modelId: webhookSubscription.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: {
|
||||
name: webhookSubscription.name,
|
||||
url: webhookSubscription.url,
|
||||
events: webhookSubscription.events,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
};
|
||||
await Event.create(event);
|
||||
const event: WebhookSubscriptionEvent = {
|
||||
name: "webhook_subscriptions.delete",
|
||||
modelId: webhookSubscription.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: {
|
||||
name: webhookSubscription.name,
|
||||
url: webhookSubscription.url,
|
||||
events: webhookSubscription.events,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
};
|
||||
await Event.create(event);
|
||||
}
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
data: presentWebhookSubscription(webhookSubscription),
|
||||
};
|
||||
});
|
||||
router.post(
|
||||
"webhookSubscriptions.update",
|
||||
auth({ admin: true }),
|
||||
async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
const { user } = ctx.state;
|
||||
|
||||
const { name, url } = ctx.request.body;
|
||||
const events: string[] = compact(ctx.request.body.events);
|
||||
assertPresent(name, "name is required");
|
||||
assertPresent(url, "url is required");
|
||||
assertArray(events, "events is required");
|
||||
if (events.length === 0) {
|
||||
throw ValidationError("events are required");
|
||||
}
|
||||
|
||||
const webhookSubscription = await WebhookSubscription.findByPk(id);
|
||||
|
||||
authorize(user, "update", webhookSubscription);
|
||||
|
||||
await webhookSubscription.update({ name, url, events, enabled: true });
|
||||
|
||||
const event: WebhookSubscriptionEvent = {
|
||||
name: "webhook_subscriptions.update",
|
||||
modelId: webhookSubscription.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: {
|
||||
name: webhookSubscription.name,
|
||||
url: webhookSubscription.url,
|
||||
events: webhookSubscription.events,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
};
|
||||
await Event.create(event);
|
||||
|
||||
ctx.body = {
|
||||
data: presentWebhookSubscription(webhookSubscription),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -117,7 +117,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
router.get(
|
||||
"slack.commands",
|
||||
auth({
|
||||
required: false,
|
||||
optional: true,
|
||||
}),
|
||||
async (ctx) => {
|
||||
const { code, state, error } = ctx.request.query;
|
||||
@@ -135,9 +135,11 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
if (!user) {
|
||||
if (state) {
|
||||
try {
|
||||
const team = await Team.findByPk(state as string);
|
||||
const team = await Team.findByPk(String(state), {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
return ctx.redirect(
|
||||
`${team!.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
@@ -152,8 +154,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
}
|
||||
|
||||
const endpoint = `${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 data = await Slack.oauthAccess(String(code), endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
userId: user.id,
|
||||
@@ -178,7 +179,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
router.get(
|
||||
"slack.post",
|
||||
auth({
|
||||
required: false,
|
||||
optional: true,
|
||||
}),
|
||||
async (ctx) => {
|
||||
const { code, error, state } = ctx.request.query;
|
||||
@@ -198,10 +199,17 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
// appropriate subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
try {
|
||||
const collection = await Collection.findByPk(state as string);
|
||||
const team = await Team.findByPk(collection!.teamId);
|
||||
const collection = await Collection.findOne({
|
||||
where: {
|
||||
id: String(state),
|
||||
},
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
const team = await Team.findByPk(collection.teamId, {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
return ctx.redirect(
|
||||
`${team!.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(
|
||||
|
||||
Reference in New Issue
Block a user