refactor: add server-side validation schema for events (#4622)
* refactor: move files to subfolder * refactor: schema for events.list * refactor: update nullable fields in Event model * fix: event actor not nullable * fix: team not nullable
This commit is contained in:
committed by
GitHub
parent
05a4f050bb
commit
dc795604a4
@@ -25,7 +25,7 @@ import Fix from "./decorators/Fix";
|
|||||||
class Event extends IdModel {
|
class Event extends IdModel {
|
||||||
@IsUUID(4)
|
@IsUUID(4)
|
||||||
@Column(DataType.UUID)
|
@Column(DataType.UUID)
|
||||||
modelId: string;
|
modelId: string | null;
|
||||||
|
|
||||||
@Length({
|
@Length({
|
||||||
max: 255,
|
max: 255,
|
||||||
@@ -39,7 +39,7 @@ class Event extends IdModel {
|
|||||||
ip: string | null;
|
ip: string | null;
|
||||||
|
|
||||||
@Column(DataType.JSONB)
|
@Column(DataType.JSONB)
|
||||||
data: Record<string, any>;
|
data: Record<string, any> | null;
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
|
|
||||||
@@ -63,18 +63,18 @@ class Event extends IdModel {
|
|||||||
// associations
|
// associations
|
||||||
|
|
||||||
@BelongsTo(() => User, "userId")
|
@BelongsTo(() => User, "userId")
|
||||||
user: User;
|
user: User | null;
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
@ForeignKey(() => User)
|
||||||
@Column(DataType.UUID)
|
@Column(DataType.UUID)
|
||||||
userId: string;
|
userId: string | null;
|
||||||
|
|
||||||
@BelongsTo(() => Document, "documentId")
|
@BelongsTo(() => Document, "documentId")
|
||||||
document: Document;
|
document: Document | null;
|
||||||
|
|
||||||
@ForeignKey(() => Document)
|
@ForeignKey(() => Document)
|
||||||
@Column(DataType.UUID)
|
@Column(DataType.UUID)
|
||||||
documentId: string;
|
documentId: string | null;
|
||||||
|
|
||||||
@BelongsTo(() => User, "actorId")
|
@BelongsTo(() => User, "actorId")
|
||||||
actor: User;
|
actor: User;
|
||||||
@@ -84,11 +84,11 @@ class Event extends IdModel {
|
|||||||
actorId: string;
|
actorId: string;
|
||||||
|
|
||||||
@BelongsTo(() => Collection, "collectionId")
|
@BelongsTo(() => Collection, "collectionId")
|
||||||
collection: Collection;
|
collection: Collection | null;
|
||||||
|
|
||||||
@ForeignKey(() => Collection)
|
@ForeignKey(() => Collection)
|
||||||
@Column(DataType.UUID)
|
@Column(DataType.UUID)
|
||||||
collectionId: string;
|
collectionId: string | null;
|
||||||
|
|
||||||
@BelongsTo(() => Team, "teamId")
|
@BelongsTo(() => Team, "teamId")
|
||||||
team: Team;
|
team: Team;
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import Router from "koa-router";
|
|
||||||
import { Op, WhereOptions } from "sequelize";
|
|
||||||
import auth from "@server/middlewares/authentication";
|
|
||||||
import { Event, User, Collection } from "@server/models";
|
|
||||||
import { authorize } from "@server/policies";
|
|
||||||
import { presentEvent } from "@server/presenters";
|
|
||||||
import { assertSort, assertUuid } from "@server/validation";
|
|
||||||
import pagination from "./middlewares/pagination";
|
|
||||||
|
|
||||||
const router = new Router();
|
|
||||||
|
|
||||||
router.post("events.list", auth(), pagination(), async (ctx) => {
|
|
||||||
const { user } = ctx.state;
|
|
||||||
let { direction } = ctx.request.body;
|
|
||||||
const {
|
|
||||||
sort = "createdAt",
|
|
||||||
actorId,
|
|
||||||
documentId,
|
|
||||||
collectionId,
|
|
||||||
name,
|
|
||||||
auditLog = false,
|
|
||||||
} = ctx.request.body;
|
|
||||||
if (direction !== "ASC") {
|
|
||||||
direction = "DESC";
|
|
||||||
}
|
|
||||||
assertSort(sort, Event);
|
|
||||||
|
|
||||||
let where: WhereOptions<Event> = {
|
|
||||||
name: Event.ACTIVITY_EVENTS,
|
|
||||||
teamId: user.teamId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (actorId) {
|
|
||||||
assertUuid(actorId, "actorId must be a UUID");
|
|
||||||
where = { ...where, actorId };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (documentId) {
|
|
||||||
assertUuid(documentId, "documentId must be a UUID");
|
|
||||||
where = { ...where, documentId };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auditLog) {
|
|
||||||
authorize(user, "manage", user.team);
|
|
||||||
where.name = Event.AUDIT_EVENTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name && (where.name as string[]).includes(name)) {
|
|
||||||
where.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionId) {
|
|
||||||
assertUuid(collectionId, "collection must be a UUID");
|
|
||||||
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.is]: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = await Event.findAll({
|
|
||||||
where,
|
|
||||||
order: [[sort, direction]],
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "actor",
|
|
||||||
paranoid: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
offset: ctx.state.pagination.offset,
|
|
||||||
limit: ctx.state.pagination.limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.body = {
|
|
||||||
pagination: ctx.state.pagination,
|
|
||||||
data: await Promise.all(
|
|
||||||
events.map((event) => presentEvent(event, auditLog))
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
102
server/routes/api/events/events.ts
Normal file
102
server/routes/api/events/events.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import Router from "koa-router";
|
||||||
|
import { Op, WhereOptions } from "sequelize";
|
||||||
|
import auth from "@server/middlewares/authentication";
|
||||||
|
import validate from "@server/middlewares/validate";
|
||||||
|
import { Event, User, Collection } from "@server/models";
|
||||||
|
import { authorize } from "@server/policies";
|
||||||
|
import { presentEvent } from "@server/presenters";
|
||||||
|
import { APIContext } from "@server/types";
|
||||||
|
import pagination from "../middlewares/pagination";
|
||||||
|
import * as T from "./schema";
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"events.list",
|
||||||
|
auth(),
|
||||||
|
pagination(),
|
||||||
|
validate(T.EventsListSchema),
|
||||||
|
async (ctx: APIContext<T.EventsListReq>) => {
|
||||||
|
const { user } = ctx.state;
|
||||||
|
const {
|
||||||
|
sort,
|
||||||
|
direction,
|
||||||
|
actorId,
|
||||||
|
documentId,
|
||||||
|
collectionId,
|
||||||
|
name,
|
||||||
|
auditLog,
|
||||||
|
} = ctx.input;
|
||||||
|
|
||||||
|
let where: WhereOptions<Event> = {
|
||||||
|
name: Event.ACTIVITY_EVENTS,
|
||||||
|
teamId: user.teamId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (actorId) {
|
||||||
|
where = { ...where, actorId };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (documentId) {
|
||||||
|
where = { ...where, documentId };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auditLog) {
|
||||||
|
authorize(user, "manage", user.team);
|
||||||
|
where.name = Event.AUDIT_EVENTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name && (where.name as string[]).includes(name)) {
|
||||||
|
where.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionId) {
|
||||||
|
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.is]: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await Event.findAll({
|
||||||
|
where,
|
||||||
|
order: [[sort, direction]],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "actor",
|
||||||
|
paranoid: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
offset: ctx.state.pagination.offset,
|
||||||
|
limit: ctx.state.pagination.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
pagination: ctx.state.pagination,
|
||||||
|
data: await Promise.all(
|
||||||
|
events.map((event) => presentEvent(event, auditLog))
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
1
server/routes/api/events/index.ts
Normal file
1
server/routes/api/events/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./events";
|
||||||
32
server/routes/api/events/schema.ts
Normal file
32
server/routes/api/events/schema.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const EventsListSchema = z.object({
|
||||||
|
/** Id of the user who performed the action */
|
||||||
|
actorId: z.string().uuid().optional(),
|
||||||
|
|
||||||
|
/** Id of the document to filter the events for */
|
||||||
|
documentId: z.string().uuid().optional(),
|
||||||
|
|
||||||
|
/** Id of the collection to filter the events for */
|
||||||
|
collectionId: z.string().uuid().optional(),
|
||||||
|
|
||||||
|
/** Whether to include audit events */
|
||||||
|
auditLog: z.boolean().default(false),
|
||||||
|
|
||||||
|
/** Name of the event to retrieve */
|
||||||
|
name: z.string().optional(),
|
||||||
|
|
||||||
|
/** The attribute to sort the events by */
|
||||||
|
sort: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ["name", "createdAt"].includes(val))
|
||||||
|
.default("createdAt"),
|
||||||
|
|
||||||
|
/** The direction to sort the events */
|
||||||
|
direction: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => (val !== "ASC" ? "DESC" : val)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EventsListReq = z.infer<typeof EventsListSchema>;
|
||||||
Reference in New Issue
Block a user