diff --git a/server/package-lock.json b/server/package-lock.json index a162456..957a940 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@prisma/client": "^4.0.0", "body-parser": "^1.20.0", + "class-validator": "^0.13.2", "dotenv": "^16.0.1", "express": "^4.18.1", "express-rate-limit": "^6.4.0", @@ -930,6 +931,15 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "node_modules/class-validator": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.13.2.tgz", + "integrity": "sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==", + "dependencies": { + "libphonenumber-js": "^1.9.43", + "validator": "^13.7.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -2935,6 +2945,11 @@ "node": ">=8" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.7.tgz", + "integrity": "sha512-jZXLCCWMe1b/HXkjiLeYt2JsytZMcqH26jLFIdzFDFF0xvSUWrYKyvPlyPG+XJzEyKUFbcZxLdWGMwQsWaHDxQ==" + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -5260,6 +5275,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6265,6 +6288,15 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "class-validator": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.13.2.tgz", + "integrity": "sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==", + "requires": { + "libphonenumber-js": "^1.9.43", + "validator": "^13.7.0" + } + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7685,6 +7717,11 @@ "package-json": "^6.3.0" } }, + "libphonenumber-js": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.7.tgz", + "integrity": "sha512-jZXLCCWMe1b/HXkjiLeYt2JsytZMcqH26jLFIdzFDFF0xvSUWrYKyvPlyPG+XJzEyKUFbcZxLdWGMwQsWaHDxQ==" + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -9417,6 +9454,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/server/package.json b/server/package.json index 6f45e4b..846a957 100644 --- a/server/package.json +++ b/server/package.json @@ -18,6 +18,7 @@ "dependencies": { "@prisma/client": "^4.0.0", "body-parser": "^1.20.0", + "class-validator": "^0.13.2", "dotenv": "^16.0.1", "express": "^4.18.1", "express-rate-limit": "^6.4.0", diff --git a/server/src/app.integration.test.ts b/server/src/app.integration.test.ts index bc5298c..8012ce7 100644 --- a/server/src/app.integration.test.ts +++ b/server/src/app.integration.test.ts @@ -3,9 +3,10 @@ import request from "supertest"; import { describe, it, expect } from "vitest"; import prisma from "./client"; +// const testNote with base64 ciphertext and hmac const testNote = { - ciphertext: "sample_ciphertext", - hmac: "sample_hmac", + ciphertext: Buffer.from("sample_ciphertext").toString("base64"), + hmac: Buffer.from("sample_hmac").toString("base64"), }; describe("GET /api/note", () => { @@ -78,6 +79,11 @@ describe("POST /api/note", () => { ); }); + it("Returns a bad request on invalid POST body", async () => { + const res = await request(app).post("/api/note").send({}); + expect(res.statusCode).toBe(400); + }); + it("returns a valid view_url on correct POST body", async () => { // Make post request let res = await request(app).post("/api/note").send(testNote); diff --git a/server/src/app.ts b/server/src/app.ts index 754ff57..a322e54 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import express, { Express, Request } from "express"; +import express, { Express, Request, Response } from "express"; import { EncryptedNote } from "@prisma/client"; import { addDays } from "./util"; import helmet from "helmet"; @@ -8,6 +8,8 @@ import pinoHttp from "pino-http"; import logger from "./logger"; import prisma from "./client"; import bodyParser from "body-parser"; +import { NotePostRequest } from "./model/NotePostRequest"; +import { validateOrReject } from "class-validator"; // Initialize middleware clients const app: Express = express(); @@ -48,7 +50,7 @@ const getLimiter = rateLimit({ app.use(bodyParser.json({ limit: "400k" })); // Get encrypted note -app.get("/api/note/:id", getLimiter, (req, res, next) => { +app.get("/api/note/:id", getLimiter, (req: Request, res: Response, next) => { prisma.encryptedNote .findUnique({ where: { id: req.params.id }, @@ -63,24 +65,28 @@ app.get("/api/note/:id", getLimiter, (req, res, next) => { }); // Post new encrypted note -app.post( - "/api/note/", - postLimiter, - (req: Request<{}, {}, EncryptedNote>, res, next) => { - const note = req.body; - prisma.encryptedNote - .create({ - data: { ...note, expire_time: addDays(new Date(), 30) }, - }) - .then((savedNote) => { - res.json({ - view_url: `${process.env.FRONTEND_URL}/note/${savedNote.id}`, - expire_time: savedNote.expire_time, - }); - }) - .catch(next); - } -); +app.post("/api/note/", postLimiter, (req: Request, res: Response, next) => { + const notePostRequest = new NotePostRequest(); + Object.assign(notePostRequest, req.body); + validateOrReject(notePostRequest).catch((err) => { + res.status(400).send(err.message); + }); + const note = notePostRequest as EncryptedNote; + prisma.encryptedNote + .create({ + data: { + ...note, + expire_time: addDays(new Date(), 30), + }, + }) + .then((savedNote) => { + res.json({ + view_url: `${process.env.FRONTEND_URL}/note/${savedNote.id}`, + expire_time: savedNote.expire_time, + }); + }) + .catch(next); +}); // For testing purposes app.get("/api/test", (req, res, next) => { diff --git a/server/src/model/NotePostRequest.ts b/server/src/model/NotePostRequest.ts new file mode 100644 index 0000000..b52a4a5 --- /dev/null +++ b/server/src/model/NotePostRequest.ts @@ -0,0 +1,12 @@ +import { IsBase64 } from "class-validator"; + +/** + * Request body for creating a note + */ +export class NotePostRequest { + @IsBase64() + ciphertext: string | undefined; + + @IsBase64() + hmac: string | undefined; +} diff --git a/server/tsconfig.json b/server/tsconfig.json index 1dc9eb6..9ffc129 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "experimentalDecorators": true, "sourceMap": true, "outDir": "./build", "lib": ["esnext"],