diff --git a/server/api/events.js b/server/api/events.js index ce39db56..7c582184 100644 --- a/server/api/events.js +++ b/server/api/events.js @@ -2,7 +2,7 @@ import Router from "koa-router"; import Sequelize from "sequelize"; import auth from "../middlewares/authentication"; -import { Event, Team, User } from "../models"; +import { Event, Team, User, Collection } from "../models"; import policy from "../policies"; import { presentEvent } from "../presenters"; import pagination from "./middlewares/pagination"; @@ -12,30 +12,62 @@ const { authorize } = policy; const router = new Router(); router.post("events.list", auth(), pagination(), async (ctx) => { - let { sort = "createdAt", direction, auditLog = false } = ctx.body; - if (direction !== "ASC") direction = "DESC"; - const user = ctx.state.user; - const collectionIds = await user.collectionIds({ paranoid: false }); + let { + sort = "createdAt", + actorId, + collectionId, + direction, + name, + auditLog = false, + } = ctx.body; + if (direction !== "ASC") direction = "DESC"; let where = { name: Event.ACTIVITY_EVENTS, teamId: user.teamId, - [Op.or]: [ - { collectionId: collectionIds }, - { - collectionId: { - [Op.eq]: null, - }, - }, - ], }; + if (actorId) { + ctx.assertUuid(actorId, "actorId must be a UUID"); + where = { + ...where, + actorId, + }; + } + + if (collectionId) { + ctx.assertUuid(collectionId, "collection must be a UUID"); + + 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 }); + where = { + ...where, + [Op.or]: [ + { collectionId: collectionIds }, + { + collectionId: { + [Op.eq]: null, + }, + }, + ], + }; + } + if (auditLog) { authorize(user, "auditLog", Team); where.name = Event.AUDIT_EVENTS; } + if (name && where.name.includes(name)) { + where.name = name; + } + const events = await Event.findAll({ where, order: [[sort, direction]], diff --git a/server/api/events.test.js b/server/api/events.test.js index a1b2d632..be915655 100644 --- a/server/api/events.test.js +++ b/server/api/events.test.js @@ -13,7 +13,7 @@ describe("#events.list", () => { it("should only return activity events", async () => { const { user, admin, document, collection } = await seed(); - // private event + // audit event await buildEvent({ name: "users.promote", teamId: user.teamId, @@ -29,6 +29,7 @@ describe("#events.list", () => { teamId: user.teamId, actorId: admin.id, }); + const res = await server.post("/api/events.list", { body: { token: user.getJwtToken() }, }); @@ -39,6 +40,100 @@ describe("#events.list", () => { expect(body.data[0].id).toEqual(event.id); }); + it("should return audit events", async () => { + const { user, admin, document, collection } = await seed(); + + // audit event + const auditEvent = await buildEvent({ + name: "users.promote", + teamId: user.teamId, + actorId: admin.id, + userId: user.id, + }); + + // event viewable in activity stream + const event = await buildEvent({ + name: "documents.publish", + collectionId: collection.id, + documentId: document.id, + teamId: user.teamId, + actorId: admin.id, + }); + + const res = await server.post("/api/events.list", { + 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); + expect(body.data[1].id).toEqual(auditEvent.id); + }); + + it("should allow filtering by actorId", async () => { + const { user, admin, document, collection } = await seed(); + + // audit event + const auditEvent = await buildEvent({ + name: "users.promote", + teamId: user.teamId, + actorId: admin.id, + userId: user.id, + }); + + // event viewable in activity stream + await buildEvent({ + name: "documents.publish", + collectionId: collection.id, + documentId: document.id, + teamId: user.teamId, + actorId: user.id, + }); + + const res = await server.post("/api/events.list", { + 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); + }); + + it("should allow filtering by event name", async () => { + const { user, admin, document, collection } = await seed(); + + // audit event + await buildEvent({ + name: "users.promote", + teamId: user.teamId, + actorId: admin.id, + userId: user.id, + }); + + // event viewable in activity stream + const event = await buildEvent({ + name: "documents.publish", + collectionId: collection.id, + documentId: document.id, + teamId: user.teamId, + actorId: user.id, + }); + + const res = await server.post("/api/events.list", { + body: { + token: user.getJwtToken(), + name: "documents.publish", + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(1); + expect(body.data[0].id).toEqual(event.id); + }); + it("should return events with deleted actors", async () => { const { user, admin, document, collection } = await seed(); @@ -64,6 +159,15 @@ describe("#events.list", () => { expect(body.data[0].id).toEqual(event.id); }); + 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 }, + }); + + expect(res.status).toEqual(403); + }); + it("should require authentication", async () => { const res = await server.post("/api/events.list"); const body = await res.json(); diff --git a/server/models/Event.js b/server/models/Event.js index c0fa2374..cefc857a 100644 --- a/server/models/Event.js +++ b/server/models/Event.js @@ -62,6 +62,7 @@ Event.ACTIVITY_EVENTS = [ "documents.unarchive", "documents.pin", "documents.unpin", + "documents.move", "documents.delete", "documents.restore", "users.create", @@ -86,6 +87,7 @@ Event.AUDIT_EVENTS = [ "documents.unpin", "documents.move", "documents.delete", + "documents.restore", "groups.create", "groups.update", "groups.delete",