diff --git a/src/index.test.ts b/src/index.test.ts index dce9497..e92ca64 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -747,6 +747,9 @@ describe('runProcess', () => { [ "✅ - Module path of "putTime" found at "file:///project/src/routes/putTime.ts".", ], + [ + "✅ - Module path of "tokenStore" found at "file:///project/src/services/tokenStore.ts".", + ], [ "✅ - Module path of "wrapRouteHandlerWithAuthorization" found at "@whook/authorization/dist/wrappers/wrapRouteHandlerWithAuthorization.js".", ], @@ -1098,6 +1101,9 @@ describe('runProcess', () => { [ "🍀 - Trying to find "putTime" module path in "__project__".", ], + [ + "🍀 - Trying to find "tokenStore" module path in "__project__".", + ], [ "🍀 - Trying to find "uniqueId" module path in "@whook/authorization".", ], @@ -1200,6 +1206,9 @@ describe('runProcess', () => { [ "💿 - Loading "putTime" initializer from "file:///project/src/routes/putTime.ts".", ], + [ + "💿 - Loading "tokenStore" initializer from "file:///project/src/services/tokenStore.ts".", + ], [ "💿 - Loading "wrapRouteHandlerWithAuthorization" initializer from "@whook/authorization/dist/wrappers/wrapRouteHandlerWithAuthorization.js".", ], @@ -1266,6 +1275,9 @@ describe('runProcess', () => { [ "💿 - Service "putTime" found in "file:///project/src/routes/putTime.ts".", ], + [ + "💿 - Service "tokenStore" found in "file:///project/src/services/tokenStore.ts".", + ], [ "💿 - Service "wrapRouteHandlerWithAuthorization" found in "@whook/authorization/dist/wrappers/wrapRouteHandlerWithAuthorization.js".", ], @@ -1815,6 +1827,9 @@ describe('runProcess', () => { [ "🛂 - Dynamic import of "file:///project/src/services/jwtToken.ts".", ], + [ + "🛂 - Dynamic import of "file:///project/src/services/tokenStore.ts".", + ], [ "🛂 - Dynamic import of "swagger-ui-dist".", ], diff --git a/src/openAPISchema.d.ts b/src/openAPISchema.d.ts index 408c616..b4a9c95 100644 --- a/src/openAPISchema.d.ts +++ b/src/openAPISchema.d.ts @@ -67,7 +67,7 @@ declare interface operations { }; getKnockValidation: { responses: { - 200: { + 201: { body: object; }; }; @@ -80,7 +80,7 @@ declare interface operations { putKnockValidation: { requestBody: object; responses: { - 200: { + 201: { body: object; }; }; diff --git a/src/routes/getKnockValidation.ts b/src/routes/getKnockValidation.ts index 55396f8..62f846d 100644 --- a/src/routes/getKnockValidation.ts +++ b/src/routes/getKnockValidation.ts @@ -17,7 +17,7 @@ export const definition = { tags: ['system'], parameters: baseDefinition.operation.parameters, responses: { - 200: { + 201: { description: 'Success', content: { 'application/json': { diff --git a/src/routes/putKnockValidation.test.ts b/src/routes/putKnockValidation.test.ts index e1d09a6..eae4762 100644 --- a/src/routes/putKnockValidation.test.ts +++ b/src/routes/putKnockValidation.test.ts @@ -1,43 +1,68 @@ import { describe, test, beforeEach, jest, expect } from '@jest/globals'; import initPutKnockValidation from './putKnockValidation.js'; -import streamtest from 'streamtest'; import { type LogService } from 'common-services'; +import type { TokenStoreService } from '../services/tokenStore.js'; describe('putKnockValidation', () => { const log = jest.fn(); + const tokenStore: jest.Mocked = { + get: jest.fn(), + set: jest.fn(), + }; beforeEach(() => { log.mockReset(); + tokenStore.get.mockReset(); + tokenStore.set.mockReset(); }); - test('should work', async () => { + test('should validate an existing knock', async () => { + tokenStore.get.mockResolvedValue({ + pattern: 'test@example.com', + validated: false, + }); + const putKnockValidation = await initPutKnockValidation({ log, + tokenStore, }); + const response = await putKnockValidation({ - path: { - knockId: 'rtt', - }, + path: { knockId: 'rtt' }, + body: {}, + }); + + expect(response).toEqual({ + status: 201, + headers: {}, body: {}, }); - expect({ - response, - logCalls: log.mock.calls.filter(([type]) => !type.endsWith('stack')), - }).toMatchInlineSnapshot(` -{ - "logCalls": [ - [ - "warning", - "📢 - Validated knock: rtt!", - ], - ], - "response": { - "body": {}, - "headers": {}, - "status": 200, - }, -} -`); + expect(tokenStore.set).toHaveBeenCalledWith('rtt', { + pattern: 'test@example.com', + validated: true, + }); + + expect(log.mock.calls).toEqual([['warning', '📢 - Validated knock: rtt!']]); + }); + + test('should throw if knock does not exist', async () => { + tokenStore.get.mockResolvedValue(undefined); + + const putKnockValidation = await initPutKnockValidation({ + log, + tokenStore, + }); + + await expect( + putKnockValidation({ + path: { knockId: 'unknown' }, + body: {}, + }), + ).rejects.toThrow('E_UNKNOWN_KNOCK'); + + expect(log.mock.calls).toEqual([ + ['warning', '❗ - Cannot validate knock: unknown!'], + ]); }); }); diff --git a/src/routes/putKnockValidation.ts b/src/routes/putKnockValidation.ts index 903b3a6..c80930e 100644 --- a/src/routes/putKnockValidation.ts +++ b/src/routes/putKnockValidation.ts @@ -5,6 +5,12 @@ import { } from '@whook/whook'; import { type LogService } from 'common-services'; +import type { + TokenStoreService, + TokenPayload, +} from '../services/tokenStore.js'; +import { YHTTPError } from 'yhttperror'; + export const definition = { path: '/knock/{knockId}/validation', method: 'put', @@ -31,7 +37,7 @@ export const definition = { }, }, responses: { - 200: { + 201: { description: 'Successfully validated', content: { 'application/json': { @@ -45,19 +51,35 @@ export const definition = { }, } as const satisfies WhookRouteDefinition; -async function initPutKnockValidation({ log }: { log: LogService }) { +async function initPutKnockValidation({ + log, + tokenStore, +}: { + log: LogService; + tokenStore: TokenStoreService; +}) { const handler: WhookRouteTypedHandler< operations[typeof definition.operation.operationId], typeof definition > = async ({ path: { knockId }, body }) => { - // inject the token store (see the smtpServer service) - // get the pqyloqd from store set validated to true - // put the payload back to the store + const payload = await tokenStore.get(knockId); + + if (!payload) { + log('warning', `❗ - Cannot validate knock: ${knockId}!`); + throw new YHTTPError(404, 'E_UNKNOWN_KNOCK', knockId); + } + + const updatedPayload: TokenPayload = { + ...payload, + validated: true, + }; + + await tokenStore.set(knockId, updatedPayload); log('warning', `📢 - Validated knock: ${knockId}!`); return { - status: 200, + status: 201, headers: {}, body: {}, }; diff --git a/src/services/__snapshots__/API.test.ts.snap b/src/services/__snapshots__/API.test.ts.snap index 22a2810..ab13d5b 100644 --- a/src/services/__snapshots__/API.test.ts.snap +++ b/src/services/__snapshots__/API.test.ts.snap @@ -505,7 +505,7 @@ exports[`API should work 1`] = ` }, ], "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { @@ -566,7 +566,7 @@ exports[`API should work 1`] = ` "required": true, }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { diff --git a/src/services/smtpServer.ts b/src/services/smtpServer.ts index bd36ec3..6e48da3 100644 --- a/src/services/smtpServer.ts +++ b/src/services/smtpServer.ts @@ -86,16 +86,31 @@ async function initSmtpServer({ `📧 - Email details: from=${fromAddress}, to=${toAddress}, subject=${subject} (session: ${session.id}).`, ); - const token = toAddress.split('@')[0].split('+').pop(); + const token = toAddress.split('@')[0].includes('+') + ? toAddress.split('@')[0].split('+').pop() + : undefined; if (!token) { log( 'warning', `💌 - Rejected mail from ${fromAddress} since no token (session: ${session.id}).`, ); - return callback( - Object.assign(new Error('Relay denied'), { responseCode: 553 }), - ); + await sendMail({ + from: toAddress, + to: fromAddress, + subject: 'Protected mailbox', + text: `Hi! + +This mailbox is protected by SafeSend, to send emails to it, + you first need to send a knock email to: ${toAddress.split('@')[0]}+knock@${toAddress.split('@')[1]} + +Below is a copy of your original email: +Subject: ${subject}, +Content: +${text} + `, + }); + return callback(); } if (token === 'knock') {