diff --git a/package.json b/package.json index be06fed..ecd61b3 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ "@koa/cors": "^4.0.0", "@koa/router": "^12.0.0", "@solid/access-token-verifier": "^2.0.5", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", "bcryptjs": "^2.4.3", "cross-fetch": "^4.0.0", "dotenv": "^16.0.0", diff --git a/src/app.ts b/src/app.ts index ca15d0d..20669e7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import { import { getStatus } from './controllers/status' import { webhookReceiver } from './controllers/webhookReceiver' import { solidAuth } from './middlewares/solidAuth' +import { validateBody } from './middlewares/validate' const app = new Koa() const router = new Router() @@ -20,7 +21,24 @@ router ctx.response.body = 'Hello world! This is a Solid email notifier. Read more at https://github.com/openHospitalityNetwork/solid-email-notifications' }) - .post('/inbox', solidAuth, initializeIntegration) + .post( + '/inbox', + solidAuth, + validateBody({ + type: 'object', + properties: { + '@context': { const: 'https://www.w3.org/ns/activitystreams' }, + '@id': { type: 'string' }, + '@type': { const: 'Add' }, + actor: { type: 'string', format: 'uri' }, + object: { type: 'string', format: 'uri' }, + target: { type: 'string', format: 'email' }, + }, + required: ['@context', '@type', 'actor', 'object', 'target'], + additionalProperties: false, + }), + initializeIntegration, + ) .get('/verify-email', checkVerificationLink, finishIntegration) .post('/webhook-receiver', webhookReceiver) .get('/status', solidAuth, getStatus) diff --git a/src/middlewares/solidAuth.ts b/src/middlewares/solidAuth.ts index 4cbb42a..4f1d99c 100644 --- a/src/middlewares/solidAuth.ts +++ b/src/middlewares/solidAuth.ts @@ -26,7 +26,7 @@ export const solidAuth: Middleware = async (ctx, next) => { (error as Error).message }` - ctx.throw(401, { error: message }) + ctx.throw(401, message) } // on success continue diff --git a/src/middlewares/validate.ts b/src/middlewares/validate.ts new file mode 100644 index 0000000..59b78ca --- /dev/null +++ b/src/middlewares/validate.ts @@ -0,0 +1,20 @@ +import addFormats from 'ajv-formats' +import Ajv2020 from 'ajv/dist/2020' +import type { Middleware } from 'koa' + +const ajv = new Ajv2020({ allErrors: true }) +addFormats(ajv) + +export const validateBody = + (schema: Parameters[0]): Middleware => + async (ctx, next) => { + const validate = ajv.compile(schema) + const isValid = validate(ctx.request.body) + + if (isValid) return await next() + else { + ctx.response.status = 400 + ctx.response.type = 'json' + ctx.response.body = validate.errors + } + } diff --git a/src/test/integration-start.spec.ts b/src/test/integration-start.spec.ts index 142410a..9b25b0e 100644 --- a/src/test/integration-start.spec.ts +++ b/src/test/integration-start.spec.ts @@ -51,6 +51,26 @@ describe('Mailer integration via /inbox', () => { expect(response.status).to.equal(200) }) + it('[invalid request body] should respond with 400', async () => { + const response = await authenticatedFetch(`${baseUrl}/inbox`, { + method: 'post', + headers: { + 'content-type': + 'application/ld+json;profile="https://www.w3.org/ns/activitystreams"', + }, + body: JSON.stringify({ + '@context': 'https://www.w3.org/ns/activitystrams', + '@id': '', + '@type': 'ads', + actor: 'asdf', + object: '/inbox', + target: 'yay', + }), + }) + + expect(response.status).to.equal(400) + }) + context('person not signed in', () => { it('should respond with 401', async () => { const response = await fetch(`${baseUrl}/inbox`, { @@ -89,16 +109,6 @@ describe('Mailer integration via /inbox', () => { }) }) - it('should check that the person requesting is the authenticated person', async () => { - const response = await authenticatedFetch(`${baseUrl}/inbox`, { - method: 'post', - headers: { 'content-type': 'application/ld+json' }, - body: JSON.stringify({}), - }) - - expect(response.status).to.equal(403) - }) - it( 'should check that the inbox belongs to the person requesting subscription', ) diff --git a/yarn.lock b/yarn.lock index ecb187a..cc9d414 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3089,6 +3089,13 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -3099,6 +3106,16 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.0.0, ajv@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -5109,6 +5126,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -6473,6 +6495,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"