From f144fe97a509b6c3dd507165cbbff02d32e759cc Mon Sep 17 00:00:00 2001 From: Douglas DUTEIL Date: Fri, 18 Oct 2024 11:53:54 +0200 Subject: [PATCH] feat(email): use smtp protocol --- Dockerfile | 2 + cypress/e2e/delete_totp/fixtures.sql | 8 +- cypress/e2e/delete_totp/index.cy.ts | 69 ++---- package-lock.json | 200 ++++++++++++++++++ package.json | 7 + .../email/src/Delete2faProtection.stories.tsx | 16 ++ packages/email/src/Delete2faProtection.tsx | 31 +++ packages/email/src/index.ts | 1 + src/config/env.ts | 1 + src/config/env.zod.ts | 1 + src/connectors/brevo.ts | 2 - src/connectors/mail.ts | 22 ++ src/controllers/main.ts | 2 +- src/controllers/totp.ts | 2 +- src/managers/user.ts | 41 ++-- src/views/mails/delete-free-totp.ejs | 18 -- test/env.zod.test.ts | 1 + tsconfig.json | 3 +- 18 files changed, 339 insertions(+), 88 deletions(-) create mode 100644 packages/email/src/Delete2faProtection.stories.tsx create mode 100644 packages/email/src/Delete2faProtection.tsx create mode 100644 src/connectors/mail.ts delete mode 100644 src/views/mails/delete-free-totp.ejs diff --git a/Dockerfile b/Dockerfile index 2332f8754..2cb01c40c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ FROM base AS prod-deps RUN corepack enable RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ + --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ --mount=type=cache,target=/root/.npm \ npm ci --omit=dev @@ -14,6 +15,7 @@ RUN corepack enable ENV CYPRESS_INSTALL_BINARY=0 RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=package-lock.json,target=package-lock.json \ + --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ --mount=type=cache,target=/root/.npm \ npm ci COPY tsconfig.json vite.config.mjs ./ diff --git a/cypress/e2e/delete_totp/fixtures.sql b/cypress/e2e/delete_totp/fixtures.sql index 333366506..6e93fb3d0 100644 --- a/cypress/e2e/delete_totp/fixtures.sql +++ b/cypress/e2e/delete_totp/fixtures.sql @@ -2,15 +2,15 @@ INSERT INTO users (id, email, email_verified, email_verified_at, encrypted_password, created_at, updated_at, given_name, family_name, phone_number, job, encrypted_totp_key, totp_key_verified_at, force_2fa) VALUES - (1, 'eab4ab97-875d-4ec7-bdcc-04323948ee63@mailslurp.com', true, CURRENT_TIMESTAMP, + (1, 'rogal.dorn@imperialfists.wh40k', true, CURRENT_TIMESTAMP, '$2a$10$kzY3LINL6..50Fy9shWCcuNlRfYq0ft5lS.KCcJ5PzrhlWfKK4NIO', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, - 'Jean', 'Jean', '0123456789', 'Sbire', + 'Rogal', 'Dorn', 'VII', 'Primarque', 'kuOSXGk68H2B3pYnph0uyXAHrmpbWaWyX/iX49xVaUc=.VMPBZSO+eAng7mjS.cI2kRY9rwhXchcKiiaMZIg==', CURRENT_TIMESTAMP, true ), - (2, 'c9fabb94-9274-4ece-a3d0-54b1987c8588@mailslurp.com', true, CURRENT_TIMESTAMP, + (2, 'konrad.curze@nightlords.wh40k', true, CURRENT_TIMESTAMP, '$2a$10$kzY3LINL6..50Fy9shWCcuNlRfYq0ft5lS.KCcJ5PzrhlWfKK4NIO', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, - 'Jean2', 'Jean2', '0123456789', 'Sbire', + 'Konrad', 'Curze', 'VIII', 'Primarque', 'kuOSXGk68H2B3pYnph0uyXAHrmpbWaWyX/iX49xVaUc=.VMPBZSO+eAng7mjS.cI2kRY9rwhXchcKiiaMZIg==', CURRENT_TIMESTAMP, true ); diff --git a/cypress/e2e/delete_totp/index.cy.ts b/cypress/e2e/delete_totp/index.cy.ts index 19a4003cb..f8cbe6783 100644 --- a/cypress/e2e/delete_totp/index.cy.ts +++ b/cypress/e2e/delete_totp/index.cy.ts @@ -1,21 +1,8 @@ describe("delete TOTP connexion", () => { - before(() => { - cy.mailslurp().then((mailslurp) => - mailslurp.inboxController.deleteAllInboxEmails({ - inboxId: "eab4ab97-875d-4ec7-bdcc-04323948ee63", - }), - ); - cy.mailslurp().then((mailslurp) => - mailslurp.inboxController.deleteAllInboxEmails({ - inboxId: "c9fabb94-9274-4ece-a3d0-54b1987c8588", - }), - ); - }); - it("should delete TOTP application", function () { cy.visit("/connection-and-account"); - cy.mfaLogin("eab4ab97-875d-4ec7-bdcc-04323948ee63@mailslurp.com"); + cy.mfaLogin("rogal.dorn@imperialfists.wh40k"); cy.contains("Configurer un code à usage unique"); @@ -23,27 +10,21 @@ describe("delete TOTP connexion", () => { cy.contains("L’application d’authentification a bien été supprimée."); - cy.mailslurp() - // use inbox id and a timeout of 30 seconds - .then((mailslurp) => - mailslurp.waitForLatestEmail( - "eab4ab97-875d-4ec7-bdcc-04323948ee63", - 60000, - true, - ), - ) - // check subject of deletion email - .then((email) => { - expect(email.subject).to.include( - "Suppression d'une application d'authentification à double facteur", - ); - }); + cy.maildevGetMessageBySubject( + "Suppression d'une application d'authentification à double facteur", + ).then((email) => { + cy.maildevVisitMessageById(email.id); + cy.contains( + "L'application a été supprimée comme étape de connexion à deux facteurs.", + ); + cy.maildevDeleteMessageById(email.id); + }); }); it("should not be ask to sign with TOTP", function () { cy.visit("http://localhost:4000"); cy.get("button.proconnect-button").click(); - cy.login("eab4ab97-875d-4ec7-bdcc-04323948ee63@mailslurp.com"); + cy.login("rogal.dorn@imperialfists.wh40k"); cy.contains('"amr": [\n "pwd"\n ],'); }); @@ -51,33 +32,27 @@ describe("delete TOTP connexion", () => { it("should disable TOTP", function () { cy.visit("/connection-and-account"); - cy.mfaLogin("c9fabb94-9274-4ece-a3d0-54b1987c8588@mailslurp.com"); + cy.mfaLogin("konrad.curze@nightlords.wh40k"); cy.contains("Validation en deux étapes"); cy.contains("Désactiver la validation en deux étapes").click(); - cy.mailslurp() - // use inbox id and a timeout of 30 seconds - .then((mailslurp) => - mailslurp.waitForLatestEmail( - "c9fabb94-9274-4ece-a3d0-54b1987c8588", - 60000, - true, - ), - ) - // check subject of deletion email - .then((email) => { - expect(email.subject).to.include( - "Désactivation de la validation en deux étapes", - ); - }); + cy.maildevGetMessageBySubject( + "Désactivation de la validation en deux étapes", + ).then((email) => { + cy.maildevVisitMessageById(email.id); + cy.contains( + "Votre compte ProConnect n'est plus protégé par la validation en deux étapes.", + ); + cy.maildevDeleteMessageById(email.id); + }); }); it("should not be ask to sign with TOTP", function () { cy.visit("http://localhost:4000"); cy.get("button.proconnect-button").click(); - cy.login("c9fabb94-9274-4ece-a3d0-54b1987c8588@mailslurp.com"); + cy.login("konrad.curze@nightlords.wh40k"); cy.contains('"amr": [\n "pwd"\n ],'); }); diff --git a/package-lock.json b/package-lock.json index 9b9d037a5..45f86e803 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,10 @@ "@dotenvx/dotenvx": "^1.19.2", "@fullhuman/postcss-purgecss": "^6.0.0", "@gouvfr/dsfr": "^1.12.1", + "@kitajs/html": "^4.2.4", + "@kitajs/ts-html-plugin": "^4.1.0", "@numerique-gouv/crisp": "https://github.com/douglasduteil/crisp/releases/download/v1.6.1/douglasduteil-crisp-1.6.1.tgz", + "@numerique-gouv/moncomptepro.email": "workspace:*", "@panva/jose": "^1.9.3", "@sentry/node": "^7.112.2", "@sentry/tracing": "^7.114.0", @@ -46,6 +49,7 @@ "express-basic-auth": "^1.2.1", "express-session": "^1.18.1", "helmet": "^7.1.0", + "html-to-text": "^9.0.5", "http-errors": "^2.0.0", "ioredis": "^5.4.1", "is-disposable-email-domain": "^1.0.7", @@ -57,6 +61,7 @@ "nanoid": "^3.3.6", "nocache": "^4.0.0", "node-pg-migrate": "^7.6.1", + "nodemailer": "^6.9.15", "npm-run-all2": "^6.1.2", "oidc-provider": "^8.5.1", "pg": "^8.13.0", @@ -74,11 +79,13 @@ "@sinonjs/fake-timers": "^11.2.2", "@types/chai": "^5.0.0", "@types/chai-as-promised": "^7.1.8", + "@types/html-to-text": "^9.0.4", "@types/http-errors": "^2.0.4", "@types/lodash": "^4.17.10", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.7", "@types/node": "^22.1.0", + "@types/nodemailer": "^6.4.16", "@types/oidc-provider": "^8.5.2", "@types/qrcode": "^1.5.5", "@types/sinonjs__fake-timers": "^8.1.5", @@ -1286,6 +1293,19 @@ "win32" ] }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/@sentry-internal/tracing": { "version": "7.112.2", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.112.2.tgz", @@ -1703,6 +1723,13 @@ "@types/express": "*" } }, + "node_modules/@types/html-to-text": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz", + "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-assert": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", @@ -1796,6 +1823,16 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.16", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", + "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/oidc-provider": { "version": "8.5.2", "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.5.2.tgz", @@ -3455,6 +3492,15 @@ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -3543,6 +3589,61 @@ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -3752,6 +3853,18 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -4659,6 +4772,41 @@ "node": ">=16.0.0" } }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", @@ -5343,6 +5491,15 @@ "node": "> 0.8" } }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", @@ -6063,6 +6220,15 @@ } } }, + "node_modules/nodemailer": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6410,6 +6576,19 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6482,6 +6661,15 @@ "node": ">= 14.16" } }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -7312,6 +7500,18 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", diff --git a/package.json b/package.json index 3b49fe282..8d4ad110e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,9 @@ "@fullhuman/postcss-purgecss": "^6.0.0", "@gouvfr/dsfr": "^1.12.1", "@numerique-gouv/crisp": "https://github.com/douglasduteil/crisp/releases/download/v1.6.1/douglasduteil-crisp-1.6.1.tgz", + "@numerique-gouv/moncomptepro.email": "workspace:*", + "@kitajs/html": "^4.2.4", + "@kitajs/ts-html-plugin": "^4.1.0", "@panva/jose": "^1.9.3", "@sentry/node": "^7.112.2", "@sentry/tracing": "^7.114.0", @@ -72,6 +75,7 @@ "express-basic-auth": "^1.2.1", "express-session": "^1.18.1", "helmet": "^7.1.0", + "html-to-text": "^9.0.5", "http-errors": "^2.0.0", "ioredis": "^5.4.1", "is-disposable-email-domain": "^1.0.7", @@ -83,6 +87,7 @@ "nanoid": "^3.3.6", "nocache": "^4.0.0", "node-pg-migrate": "^7.6.1", + "nodemailer": "^6.9.15", "npm-run-all2": "^6.1.2", "oidc-provider": "^8.5.1", "pg": "^8.13.0", @@ -100,11 +105,13 @@ "@sinonjs/fake-timers": "^11.2.2", "@types/chai": "^5.0.0", "@types/chai-as-promised": "^7.1.8", + "@types/html-to-text": "^9.0.4", "@types/http-errors": "^2.0.4", "@types/lodash": "^4.17.10", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.7", "@types/node": "^22.1.0", + "@types/nodemailer": "^6.4.16", "@types/oidc-provider": "^8.5.2", "@types/qrcode": "^1.5.5", "@types/sinonjs__fake-timers": "^8.1.5", diff --git a/packages/email/src/Delete2faProtection.stories.tsx b/packages/email/src/Delete2faProtection.stories.tsx new file mode 100644 index 000000000..7158cd38a --- /dev/null +++ b/packages/email/src/Delete2faProtection.stories.tsx @@ -0,0 +1,16 @@ +// + +import type { ComponentAnnotations, Renderer } from "@storybook/csf"; +import Delete2faProtection, { type Props } from "./Delete2faProtection"; + +// + +export default { + title: "Delete 2FA Protection", + render: Delete2faProtection, + args: { + given_name: "Marie", + family_name: "Dupont", + baseurl: "http://localhost:3000", + } as Props, +} as ComponentAnnotations; diff --git a/packages/email/src/Delete2faProtection.tsx b/packages/email/src/Delete2faProtection.tsx new file mode 100644 index 000000000..b22d7f089 --- /dev/null +++ b/packages/email/src/Delete2faProtection.tsx @@ -0,0 +1,31 @@ +// + +import { Layout, type LayoutProps } from "./_layout"; +import { Text } from "./components"; + +// + +export default function Delete2faProtection(props: Props) { + const { baseurl, given_name, family_name } = props; + return ( + + + Bonjour {given_name} {family_name}, + +
+ + Votre compte ProConnect n'est plus protégé par la validation en deux + étapes. +
+ Vous n'avez pas besoin de votre deuxième facteur pour vous connecter. +
+
+ ); +} + +// + +export type Props = LayoutProps & { + given_name: string; + family_name: string; +}; diff --git a/packages/email/src/index.ts b/packages/email/src/index.ts index 81cde342d..917e619e8 100644 --- a/packages/email/src/index.ts +++ b/packages/email/src/index.ts @@ -1,4 +1,5 @@ // +export { default as Delete2faProtection } from "./Delete2faProtection"; export { default as DeleteFreeTotpMail } from "./DeleteFreeTotpMail"; export { default as UpdatePersonalDataMail } from "./UpdatePersonalDataMail"; diff --git a/src/config/env.ts b/src/config/env.ts index 8d838c160..37c72246c 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -74,6 +74,7 @@ export const { SENTRY_DSN, SESSION_COOKIE_SECRET, SESSION_MAX_AGE_IN_SECONDS, + SMTP_URL, SYMMETRIC_ENCRYPTION_KEY, TEST_CONTACT_EMAIL, TRUSTED_BROWSER_COOKIE_MAX_AGE_IN_SECONDS, diff --git a/src/config/env.zod.ts b/src/config/env.zod.ts index 675299659..ee630da0f 100644 --- a/src/config/env.zod.ts +++ b/src/config/env.zod.ts @@ -18,6 +18,7 @@ export const connectorEnvSchema = z.object({ INSEE_CONSUMER_SECRET: z.string(), REDIS_URL: z.string().url().default("redis://:@127.0.0.1:6379"), SENTRY_DSN: z.string().default(""), + SMTP_URL: z.string().default("smtp://localhost:1025"), }); export const featureTogglesEnvSchema = z.object({ diff --git a/src/connectors/brevo.ts b/src/connectors/brevo.ts index e2270c4cb..2679d5c34 100644 --- a/src/connectors/brevo.ts +++ b/src/connectors/brevo.ts @@ -16,8 +16,6 @@ type LocalTemplateSlug = | "welcome" | "moderation-processed" | "delete-account" - | "delete-free-totp" - | "delete-2fa-protection" | "delete-access-key" | "add-access-key" | "update-totp-application" diff --git a/src/connectors/mail.ts b/src/connectors/mail.ts new file mode 100644 index 000000000..efc6780f5 --- /dev/null +++ b/src/connectors/mail.ts @@ -0,0 +1,22 @@ +// + +import { convert } from "html-to-text"; +import { createTransport, type SendMailOptions } from "nodemailer"; +import { SMTP_URL } from "../config/env"; + +// + +const transporter = createTransport({ + url: SMTP_URL, +}); + +// + +export function sendMail(options: Omit) { + return transporter.sendMail({ + text: + typeof options.html === "string" ? convert(options.html) : options.text, + ...options, + from: "nepasrepondre@email.moncomptepro.beta.gouv.fr", + }); +} diff --git a/src/controllers/main.ts b/src/controllers/main.ts index ace07d7da..2e46113cf 100644 --- a/src/controllers/main.ts +++ b/src/controllers/main.ts @@ -201,7 +201,7 @@ export const postDisableForce2faController = async ( const updatedUser = await disableForce2fa(user_id); updateUserInAuthenticatedSession(req, updatedUser); - sendDisable2faMail({ user_id }); + await sendDisable2faMail({ user_id }); return res.redirect( `/connection-and-account?notification=2fa_successfully_disabled`, diff --git a/src/controllers/totp.ts b/src/controllers/totp.ts index 1dbc03d1c..4dffba7b1 100644 --- a/src/controllers/totp.ts +++ b/src/controllers/totp.ts @@ -123,7 +123,7 @@ export const postDeleteAuthenticatorAppConfigurationController = async ( updateUserInAuthenticatedSession(req, updatedUser); - sendDeleteFreeTOTPApplicationEmail({ user_id }); + await sendDeleteFreeTOTPApplicationEmail({ user_id }); return res.redirect( `/connection-and-account?notification=authenticator_successfully_deleted`, diff --git a/src/managers/user.ts b/src/managers/user.ts index 44ec39303..94674aa46 100644 --- a/src/managers/user.ts +++ b/src/managers/user.ts @@ -1,3 +1,7 @@ +import { + Delete2faProtection, + DeleteFreeTotpMail, +} from "@numerique-gouv/moncomptepro.email"; import { isEmpty } from "lodash-es"; import { EmailUnavailableError, @@ -11,12 +15,14 @@ import { UserNotFoundError, WeakPasswordError, } from "../config/errors"; -import { sendMail } from "../connectors/brevo"; +import { sendMail as legacySendMail } from "../connectors/brevo"; import { isEmailSafeToSendTransactional } from "../connectors/debounce"; +import { sendMail } from "../connectors/mail"; import { MAGIC_LINK_TOKEN_EXPIRATION_DURATION_IN_MINUTES, MAX_DURATION_BETWEEN_TWO_EMAIL_ADDRESS_VERIFICATION_IN_MINUTES, + MONCOMPTEPRO_HOST, RESET_PASSWORD_TOKEN_EXPIRATION_DURATION_IN_MINUTES, VERIFY_EMAIL_TOKEN_EXPIRATION_DURATION_IN_MINUTES, } from "../config/env"; @@ -178,7 +184,7 @@ export const sendEmailAddressVerificationEmail = async ({ verify_email_sent_at: new Date(), }); - await sendMail({ + await legacySendMail({ to: [user.email], subject: "Vérification de votre adresse email", template: "verify-email", @@ -197,7 +203,7 @@ export const sendDeleteUserEmail = async ({ user_id }: { user_id: number }) => { } const { given_name, family_name, email } = user; - return sendMail({ + return legacySendMail({ to: [email], subject: "Suppression de compte", template: "delete-account", @@ -220,8 +226,12 @@ export const sendDeleteFreeTOTPApplicationEmail = async ({ to: [email], subject: "Suppression d'une application d'authentification à double facteur", - template: "delete-free-totp", - params: { given_name, family_name }, + html: DeleteFreeTotpMail({ + baseurl: MONCOMPTEPRO_HOST, + family_name: family_name ?? "", + given_name: given_name ?? "", + support_email: "contact@moncomptepro.beta.gouv.fr", + }).toString(), }); }; @@ -235,8 +245,11 @@ export const sendDisable2faMail = async ({ user_id }: { user_id: number }) => { return sendMail({ to: [email], subject: "Désactivation de la validation en deux étapes", - template: "delete-2fa-protection", - params: { given_name, family_name }, + html: Delete2faProtection({ + baseurl: MONCOMPTEPRO_HOST, + family_name: family_name ?? "", + given_name: given_name ?? "", + }).toString(), }); }; @@ -250,7 +263,7 @@ export const sendChangeAppliTotpEmail = async ({ throw new UserNotFoundError(); } const { given_name, family_name, email } = user; - return sendMail({ + return legacySendMail({ to: [email], subject: "Changement d'application d’authentification", template: "update-totp-application", @@ -269,7 +282,7 @@ export const sendDeleteAccessKeyMail = async ({ } const { given_name, family_name, email } = user; - return sendMail({ + return legacySendMail({ to: [email], subject: "Alerte de sécurité", template: "delete-access-key", @@ -288,7 +301,7 @@ export const sendAddFreeTOTPEmail = async ({ } const { given_name, family_name, email } = user; - return sendMail({ + return legacySendMail({ to: [email], subject: "Validation en deux étapes activée", template: "add-2fa", @@ -307,7 +320,7 @@ export const sendActivateAccessKeyMail = async ({ } const { given_name, family_name, email } = user; - return sendMail({ + return legacySendMail({ to: [email], subject: "Alerte de sécurité", template: "add-access-key", @@ -354,7 +367,7 @@ export const sendUpdatePersonalInformationEmail = async ({ } if (previousInformations !== newInformation) { - return sendMail({ + return legacySendMail({ to: [email], subject: "Mise à jour de vos données personnelles", template: "update-personal-data", @@ -428,7 +441,7 @@ export const sendSendMagicLinkEmail = async ( magic_link_sent_at: new Date(), }); - await sendMail({ + await legacySendMail({ to: [user.email], subject: "Lien de connexion à ProConnect", template: "magic-link", @@ -487,7 +500,7 @@ export const sendResetPasswordEmail = async ( reset_password_sent_at: new Date(), }); - await sendMail({ + await legacySendMail({ to: [user.email], subject: "Instructions pour la réinitialisation du mot de passe", template: "reset-password", diff --git a/src/views/mails/delete-free-totp.ejs b/src/views/mails/delete-free-totp.ejs deleted file mode 100644 index c6a25f27a..000000000 --- a/src/views/mails/delete-free-totp.ejs +++ /dev/null @@ -1,18 +0,0 @@ -Bonjour <%= given_name %> <%= family_name %>,

-L'application a été supprimée comme étape de connexion à deux facteurs. -

- - - - Si vous n'avez pas supprimé cette application, quelqu'un utilise peut-être - votre compte. Faites-le nous savoir en répondant à cet email. - - - -

-Cordialement, -

-L’équipe ProConnect diff --git a/test/env.zod.test.ts b/test/env.zod.test.ts index 926d8142a..757fd69f7 100644 --- a/test/env.zod.test.ts +++ b/test/env.zod.test.ts @@ -70,6 +70,7 @@ test("default sample env with configured INSEE secrets", () => { SENTRY_DSN: "", SESSION_COOKIE_SECRET: ["moncompteprosecret"], SESSION_MAX_AGE_IN_SECONDS: 86400, + SMTP_URL: "smtp://localhost:1025", SYMMETRIC_ENCRYPTION_KEY: "aTrueRandom32BytesLongBase64EncodedStringAA=", TEST_CONTACT_EMAIL: "mairie@yopmail.com", TRUSTED_BROWSER_COOKIE_MAX_AGE_IN_SECONDS: 7776000, diff --git a/tsconfig.json b/tsconfig.json index 61fa99dea..a4ce8b079 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "allowJs": true, "allowSyntheticDefaultImports": true, + "jsx": "react-jsx", + "jsxImportSource": "@kitajs/html", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, @@ -16,6 +18,5 @@ "rootDir": ".", "verbatimModuleSyntax": true }, - "exclude": ["packages/*"], "extends": "@tsconfig/node20/tsconfig.json" }