chore: request validation for pins (#5465)
This commit is contained in:
@@ -1,158 +0,0 @@
|
||||
import Router from "koa-router";
|
||||
import { Sequelize, Op } from "sequelize";
|
||||
import pinCreator from "@server/commands/pinCreator";
|
||||
import pinDestroyer from "@server/commands/pinDestroyer";
|
||||
import pinUpdater from "@server/commands/pinUpdater";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Collection, Document, Pin } from "@server/models";
|
||||
import { authorize } from "@server/policies";
|
||||
import {
|
||||
presentPin,
|
||||
presentDocument,
|
||||
presentPolicies,
|
||||
} from "@server/presenters";
|
||||
import { APIContext } from "@server/types";
|
||||
import { assertUuid, assertIndexCharacters } from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post("pins.create", auth(), async (ctx: APIContext) => {
|
||||
const { documentId, collectionId } = ctx.request.body;
|
||||
const { index } = ctx.request.body;
|
||||
assertUuid(documentId, "documentId is required");
|
||||
|
||||
const { user } = ctx.state.auth;
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
if (collectionId) {
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "update", collection);
|
||||
authorize(user, "pin", document);
|
||||
} else {
|
||||
authorize(user, "pinToHome", document);
|
||||
}
|
||||
|
||||
if (index) {
|
||||
assertIndexCharacters(index);
|
||||
}
|
||||
|
||||
const pin = await pinCreator({
|
||||
user,
|
||||
documentId,
|
||||
collectionId,
|
||||
ip: ctx.request.ip,
|
||||
index,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentPin(pin),
|
||||
policies: presentPolicies(user, [pin]),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("pins.list", auth(), pagination(), async (ctx: APIContext) => {
|
||||
const { collectionId } = ctx.request.body;
|
||||
const { user } = ctx.state.auth;
|
||||
|
||||
const [pins, collectionIds] = await Promise.all([
|
||||
Pin.findAll({
|
||||
where: {
|
||||
...(collectionId
|
||||
? { collectionId }
|
||||
: { collectionId: { [Op.is]: null } }),
|
||||
teamId: user.teamId,
|
||||
},
|
||||
order: [
|
||||
Sequelize.literal('"pin"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
}),
|
||||
user.collectionIds(),
|
||||
]);
|
||||
|
||||
const documents = await Document.defaultScopeWithUser(user.id).findAll({
|
||||
where: {
|
||||
id: pins.map((pin) => pin.documentId),
|
||||
collectionId: collectionIds,
|
||||
},
|
||||
});
|
||||
|
||||
const policies = presentPolicies(user, [...documents, ...pins]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: {
|
||||
pins: pins.map(presentPin),
|
||||
documents: await Promise.all(
|
||||
documents.map((document: Document) => presentDocument(document))
|
||||
),
|
||||
},
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
router.post("pins.update", auth(), async (ctx: APIContext) => {
|
||||
const { id, index } = ctx.request.body;
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
assertIndexCharacters(index);
|
||||
|
||||
const { user } = ctx.state.auth;
|
||||
let pin = await Pin.findByPk(id, { rejectOnEmpty: true });
|
||||
|
||||
const document = await Document.findByPk(pin.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (pin.collectionId) {
|
||||
authorize(user, "pin", document);
|
||||
} else {
|
||||
authorize(user, "update", pin);
|
||||
}
|
||||
|
||||
pin = await pinUpdater({
|
||||
user,
|
||||
pin,
|
||||
ip: ctx.request.ip,
|
||||
index,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentPin(pin),
|
||||
policies: presentPolicies(user, [pin]),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("pins.delete", auth(), async (ctx: APIContext) => {
|
||||
const { id } = ctx.request.body;
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const { user } = ctx.state.auth;
|
||||
const pin = await Pin.findByPk(id, { rejectOnEmpty: true });
|
||||
|
||||
const document = await Document.findByPk(pin.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (pin.collectionId) {
|
||||
authorize(user, "unpin", document);
|
||||
} else {
|
||||
authorize(user, "delete", pin);
|
||||
}
|
||||
|
||||
await pinDestroyer({ user, pin, ip: ctx.request.ip });
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
1
server/routes/api/pins/index.ts
Normal file
1
server/routes/api/pins/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./pins";
|
||||
410
server/routes/api/pins/pins.test.ts
Normal file
410
server/routes/api/pins/pins.test.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { Collection, Document, Pin, User } from "@server/models";
|
||||
import {
|
||||
buildAdmin,
|
||||
buildCollection,
|
||||
buildDocument,
|
||||
buildDraftDocument,
|
||||
buildPin,
|
||||
buildUser,
|
||||
} from "@server/test/factories";
|
||||
import { getTestServer } from "@server/test/support";
|
||||
|
||||
const server = getTestServer();
|
||||
|
||||
describe("#pins.create", () => {
|
||||
let admin: User;
|
||||
let user: User;
|
||||
let anotherUser: User;
|
||||
let document: Document;
|
||||
let collection: Collection;
|
||||
|
||||
beforeEach(async () => {
|
||||
admin = await buildAdmin();
|
||||
[user, anotherUser] = await Promise.all([
|
||||
buildUser({ teamId: admin.teamId }),
|
||||
buildUser(),
|
||||
]);
|
||||
collection = await buildCollection({
|
||||
createdById: admin.id,
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
document = await buildDocument({
|
||||
createdById: admin.id,
|
||||
teamId: admin.teamId,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with status 401 unauthorized when user token is missing", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
documentId: "foo",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body.message).toEqual("Authentication required");
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when documentId is not suppled", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.message).toEqual("documentId: required");
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when documentId is invalid", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: "foo",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.message).toEqual("documentId: must be uuid or url slug");
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when index is invalid", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: "foo1234567",
|
||||
index: "😀",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.message).toEqual("index: must be between x20 to x7E ASCII");
|
||||
});
|
||||
|
||||
it("should fail with status 403 forbidden when user is disallowed to read the document", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: anotherUser.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body.message).toEqual("Authorization error");
|
||||
});
|
||||
|
||||
it("should fail with status 403 forbidden when user is disallowed to update the collection", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body.message).toEqual("Authorization error");
|
||||
});
|
||||
|
||||
it("should fail with status 403 forbidden when user is disallowed to pin the document", async () => {
|
||||
const draft = await buildDraftDocument({
|
||||
createdById: admin.id,
|
||||
teamId: admin.teamId,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
// A draft document cannot be pinned, neither by a member nor by an admin
|
||||
documentId: draft.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body.message).toEqual("Authorization error");
|
||||
});
|
||||
|
||||
it("should fail with status 403 forbidden when user is disallowed to pin the document to home page", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body.message).toEqual("Authorization error");
|
||||
});
|
||||
|
||||
it("should succeed with status 200 ok when user is allowed to pin", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).toBeTruthy();
|
||||
expect(body.data.documentId).toEqual(document.id);
|
||||
expect(body.data.collectionId).toBeNull();
|
||||
});
|
||||
|
||||
it("should succeed with status 200 ok when valid collectionId is supplied", async () => {
|
||||
const res = await server.post("/api/pins.create", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
documentId: document.id,
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).toBeTruthy();
|
||||
expect(body.data.documentId).toEqual(document.id);
|
||||
expect(body.data.collectionId).toEqual(collection.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#pins.list", () => {
|
||||
let user: User;
|
||||
let pins: Pin[];
|
||||
let docs: Document[];
|
||||
let collection: Collection;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await buildUser();
|
||||
collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
});
|
||||
docs = await Promise.all([
|
||||
buildDocument({
|
||||
teamId: user.teamId,
|
||||
collectionId: collection.id,
|
||||
}),
|
||||
buildDocument({
|
||||
teamId: user.teamId,
|
||||
collectionId: collection.id,
|
||||
}),
|
||||
]);
|
||||
pins = await Promise.all([
|
||||
buildPin({
|
||||
createdById: user.id,
|
||||
documentId: docs[0].id,
|
||||
teamId: user.teamId,
|
||||
}),
|
||||
buildPin({
|
||||
createdById: user.id,
|
||||
documentId: docs[1].id,
|
||||
teamId: user.teamId,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should fail with status 401 unauthorized when user token is missing", async () => {
|
||||
const res = await server.post("/api/pins.list", {
|
||||
body: {},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body.message).toEqual("Authentication required");
|
||||
});
|
||||
|
||||
it("should succeed with status 200 ok returning pinned documents", async () => {
|
||||
const res = await server.post("/api/pins.list", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).toBeTruthy();
|
||||
expect(body.data.pins).toBeTruthy();
|
||||
expect(body.data.pins).toHaveLength(2);
|
||||
const pinIds = body.data.pins.map((p: any) => p.id);
|
||||
expect(pinIds).toContain(pins[0].id);
|
||||
expect(pinIds).toContain(pins[1].id);
|
||||
const docIds = body.data.documents.map((d: any) => d.id);
|
||||
expect(docIds).toContain(docs[0].id);
|
||||
expect(docIds).toContain(docs[1].id);
|
||||
});
|
||||
|
||||
it("should succeed with status 200 ok returning pinned documents filtered by collectionId supplied", async () => {
|
||||
const res = await server.post("/api/pins.list", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).toBeTruthy();
|
||||
expect(body.data.pins).toBeTruthy();
|
||||
expect(body.data.pins).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#pins.update", () => {
|
||||
let user: User;
|
||||
let admin: User;
|
||||
let pin: Pin;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await buildUser();
|
||||
admin = await buildAdmin();
|
||||
const collection = await buildCollection({
|
||||
createdById: admin.id,
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
const doc = await buildDocument({
|
||||
createdById: admin.id,
|
||||
teamId: admin.teamId,
|
||||
collectionId: collection.id,
|
||||
});
|
||||
pin = await buildPin({
|
||||
teamId: admin.teamId,
|
||||
createdById: admin.id,
|
||||
documentId: doc.id,
|
||||
index: "a",
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with status 401 unauthorized when user token is missing", async () => {
|
||||
const res = await server.post("/api/pins.update", {
|
||||
body: {
|
||||
id: pin.id,
|
||||
index: "i",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body.message).toEqual("Authentication required");
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when id is missing", async () => {
|
||||
const res = await server.post("/api/pins.update", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.message).toEqual("id: Required");
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when index is missing", async () => {
|
||||
const res = await server.post("/api/pins.update", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: pin.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.message).toEqual("index: Required");
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when an invalid index is sent", async () => {
|
||||
const res = await server.post("/api/pins.update", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: pin.id,
|
||||
index: "😀",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.message).toEqual("index: must be between x20 to x7E ASCII");
|
||||
});
|
||||
|
||||
it("should fail with status 403 forbidden when user is disallowed to update the pin", async () => {
|
||||
const res = await server.post("/api/pins.update", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: pin.id,
|
||||
index: "b",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body.message).toEqual("Authorization error");
|
||||
});
|
||||
|
||||
it("should succeed with status 200 ok and when user is allowed to update the pin", async () => {
|
||||
const res = await server.post("/api/pins.update", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: pin.id,
|
||||
index: "b",
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data).toBeTruthy();
|
||||
expect(body.data.id).toEqual(pin.id);
|
||||
expect(body.data.index).toEqual("b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#pins.delete", () => {
|
||||
let admin: User;
|
||||
let pin: Pin;
|
||||
|
||||
beforeEach(async () => {
|
||||
admin = await buildAdmin();
|
||||
pin = await buildPin({
|
||||
teamId: admin.teamId,
|
||||
createdById: admin.id,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail with status 401 unauthorized when user token is missing", async () => {
|
||||
const res = await server.post("/api/pins.delete", {
|
||||
body: {},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body.message).toEqual("Authentication required");
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when id is missing", async () => {
|
||||
const res = await server.post("/api/pins.delete", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(400);
|
||||
expect(body.message).toEqual("id: Required");
|
||||
});
|
||||
|
||||
it("should fail with status 403 forbidden when user is disallowed to delete the pin", async () => {
|
||||
const user = await buildUser({
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
const res = await server.post("/api/pins.delete", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: pin.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body.message).toEqual("Authorization error");
|
||||
});
|
||||
|
||||
it("should succeed with status 200 ok when user is allowed to delete the pin", async () => {
|
||||
const res = await server.post("/api/pins.delete", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
id: pin.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.success).toEqual(true);
|
||||
});
|
||||
});
|
||||
168
server/routes/api/pins/pins.ts
Normal file
168
server/routes/api/pins/pins.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import Router from "koa-router";
|
||||
import { Sequelize, Op } from "sequelize";
|
||||
import pinCreator from "@server/commands/pinCreator";
|
||||
import pinDestroyer from "@server/commands/pinDestroyer";
|
||||
import pinUpdater from "@server/commands/pinUpdater";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import validate from "@server/middlewares/validate";
|
||||
import { Collection, Document, Pin } from "@server/models";
|
||||
import { authorize } from "@server/policies";
|
||||
import {
|
||||
presentPin,
|
||||
presentDocument,
|
||||
presentPolicies,
|
||||
} from "@server/presenters";
|
||||
import { APIContext } from "@server/types";
|
||||
import pagination from "../middlewares/pagination";
|
||||
import * as T from "./schema";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post(
|
||||
"pins.create",
|
||||
auth(),
|
||||
validate(T.PinsCreateSchema),
|
||||
async (ctx: APIContext<T.PinsCreateReq>) => {
|
||||
const { documentId, collectionId, index } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
if (collectionId) {
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "update", collection);
|
||||
authorize(user, "pin", document);
|
||||
} else {
|
||||
authorize(user, "pinToHome", document);
|
||||
}
|
||||
|
||||
const pin = await pinCreator({
|
||||
user,
|
||||
documentId,
|
||||
collectionId,
|
||||
ip: ctx.request.ip,
|
||||
index,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentPin(pin),
|
||||
policies: presentPolicies(user, [pin]),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"pins.list",
|
||||
auth(),
|
||||
validate(T.PinsListSchema),
|
||||
pagination(),
|
||||
async (ctx: APIContext<T.PinsCreateReq>) => {
|
||||
const { collectionId } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
|
||||
const [pins, collectionIds] = await Promise.all([
|
||||
Pin.findAll({
|
||||
where: {
|
||||
...(collectionId
|
||||
? { collectionId }
|
||||
: { collectionId: { [Op.is]: null } }),
|
||||
teamId: user.teamId,
|
||||
},
|
||||
order: [
|
||||
Sequelize.literal('"pin"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
}),
|
||||
user.collectionIds(),
|
||||
]);
|
||||
|
||||
const documents = await Document.defaultScopeWithUser(user.id).findAll({
|
||||
where: {
|
||||
id: pins.map((pin) => pin.documentId),
|
||||
collectionId: collectionIds,
|
||||
},
|
||||
});
|
||||
|
||||
const policies = presentPolicies(user, [...documents, ...pins]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: {
|
||||
pins: pins.map(presentPin),
|
||||
documents: await Promise.all(
|
||||
documents.map((document: Document) => presentDocument(document))
|
||||
),
|
||||
},
|
||||
policies,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"pins.update",
|
||||
auth(),
|
||||
validate(T.PinsUpdateSchema),
|
||||
async (ctx: APIContext<T.PinsUpdateReq>) => {
|
||||
const { id, index } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
let pin = await Pin.findByPk(id, { rejectOnEmpty: true });
|
||||
|
||||
const document = await Document.findByPk(pin.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (pin.collectionId) {
|
||||
authorize(user, "pin", document);
|
||||
} else {
|
||||
authorize(user, "update", pin);
|
||||
}
|
||||
|
||||
pin = await pinUpdater({
|
||||
user,
|
||||
pin,
|
||||
ip: ctx.request.ip,
|
||||
index,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentPin(pin),
|
||||
policies: presentPolicies(user, [pin]),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"pins.delete",
|
||||
auth(),
|
||||
validate(T.PinsDeleteSchema),
|
||||
async (ctx: APIContext<T.PinsDeleteReq>) => {
|
||||
const { id } = ctx.input.body;
|
||||
|
||||
const { user } = ctx.state.auth;
|
||||
const pin = await Pin.findByPk(id, { rejectOnEmpty: true });
|
||||
|
||||
const document = await Document.findByPk(pin.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (pin.collectionId) {
|
||||
authorize(user, "unpin", document);
|
||||
} else {
|
||||
authorize(user, "delete", pin);
|
||||
}
|
||||
|
||||
await pinDestroyer({ user, pin, ip: ctx.request.ip });
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
52
server/routes/api/pins/schema.ts
Normal file
52
server/routes/api/pins/schema.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import { z } from "zod";
|
||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
||||
import BaseSchema from "../BaseSchema";
|
||||
|
||||
export const PinsCreateSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
documentId: z
|
||||
.string({
|
||||
required_error: "required",
|
||||
})
|
||||
.refine((val) => isUUID(val) || SLUG_URL_REGEX.test(val), {
|
||||
message: "must be uuid or url slug",
|
||||
}),
|
||||
collectionId: z.string().uuid().nullish(),
|
||||
index: z
|
||||
.string()
|
||||
.regex(new RegExp("^[\x20-\x7E]+$"), {
|
||||
message: "must be between x20 to x7E ASCII",
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type PinsCreateReq = z.infer<typeof PinsCreateSchema>;
|
||||
|
||||
export const PinsListSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
collectionId: z.string().uuid().nullish(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type PinsListReq = z.infer<typeof PinsCreateSchema>;
|
||||
|
||||
export const PinsUpdateSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
id: z.string().uuid(),
|
||||
index: z.string().regex(new RegExp("^[\x20-\x7E]+$"), {
|
||||
message: "must be between x20 to x7E ASCII",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export type PinsUpdateReq = z.infer<typeof PinsUpdateSchema>;
|
||||
|
||||
export const PinsDeleteSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type PinsDeleteReq = z.infer<typeof PinsDeleteSchema>;
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
Subscription,
|
||||
Notification,
|
||||
SearchQuery,
|
||||
Pin,
|
||||
} from "@server/models";
|
||||
|
||||
let count = 1;
|
||||
@@ -548,3 +549,26 @@ export async function buildSearchQuery(
|
||||
|
||||
return SearchQuery.create(overrides);
|
||||
}
|
||||
|
||||
export async function buildPin(overrides: Partial<Pin> = {}): Promise<Pin> {
|
||||
if (!overrides.teamId) {
|
||||
const team = await buildTeam();
|
||||
overrides.teamId = team.id;
|
||||
}
|
||||
|
||||
if (!overrides.createdById) {
|
||||
const user = await buildUser({
|
||||
teamId: overrides.teamId,
|
||||
});
|
||||
overrides.createdById = user.id;
|
||||
}
|
||||
|
||||
if (!overrides.documentId) {
|
||||
const document = await buildDocument({
|
||||
teamId: overrides.teamId,
|
||||
});
|
||||
overrides.documentId = document.id;
|
||||
}
|
||||
|
||||
return Pin.create(overrides);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user