From 562485134a83d45f0e6e42dca112cec96690f30b Mon Sep 17 00:00:00 2001 From: evlist Date: Fri, 3 Mar 2023 15:57:15 +0100 Subject: [PATCH] Backend API looks fine --- package-lock.json | 139 +++++++++++++++++++- package.json | 4 + src/lib/get-url.ts | 7 +- src/lib/get.ts | 13 +- src/lib/obfuscate-mail.ts | 12 ++ src/lib/put.ts | 24 +++- src/mail-template/email.handlebars | 14 ++ src/routes/api/conf/[token]/[code]/index.ts | 20 ++- src/routes/api/conf/[token]/index.ts | 53 +++++++- 9 files changed, 266 insertions(+), 20 deletions(-) create mode 100644 src/lib/obfuscate-mail.ts create mode 100644 src/mail-template/email.handlebars diff --git a/package-lock.json b/package-lock.json index 92d0889..7b1a46d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@kobalte/core": "^0.6.2", "@types/pouchdb": "^6.4.0", "lodash": "^4.17.21", + "nodemailer": "^6.9.1", + "nodemailer-express-handlebars": "^6.0.0", "pouchdb": "^8.0.1", "pouchdb-server": "^4.2.0", "tippy.js": "^6.3.7", @@ -23,6 +25,8 @@ "@solidjs/testing-library": "^0.6.1", "@testing-library/jest-dom": "^5.16.5", "@types/lodash": "^4.14.191", + "@types/nodemailer": "^6.4.7", + "@types/nodemailer-express-handlebars": "^4.0.2", "@types/testing-library__jest-dom": "^5.14.5", "@types/uuid": "^9.0.1", "@vitest/coverage-c8": "^0.29.2", @@ -2948,6 +2952,12 @@ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", "dev": true }, + "node_modules/@types/express-handlebars": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz", + "integrity": "sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -3031,6 +3041,25 @@ "integrity": "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==", "dev": true }, + "node_modules/@types/nodemailer": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.7.tgz", + "integrity": "sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/nodemailer-express-handlebars": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/nodemailer-express-handlebars/-/nodemailer-express-handlebars-4.0.2.tgz", + "integrity": "sha512-LnKnqgl6C3osQKQDcIxB6P4iS2Iixq+p0ZCC93pzhSRLvo4PgGHmrTFE38ZGrFxc/DyZefYGEsYHSblxJtwuxw==", + "dev": true, + "dependencies": { + "@types/express-handlebars": "^5", + "@types/nodemailer": "*" + } + }, "node_modules/@types/pouchdb": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@types/pouchdb/-/pouchdb-6.4.0.tgz", @@ -5497,6 +5526,56 @@ "node": ">= 0.10.0" } }, + "node_modules/express-handlebars": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-6.0.7.tgz", + "integrity": "sha512-iYeMFpc/hMD+E6FNAZA5fgWeXnXr4rslOSPkeEV6TwdmpJ5lEXuWX0u9vFYs31P2MURctQq2batR09oeNj0LIg==", + "dependencies": { + "glob": "^8.1.0", + "graceful-fs": "^4.2.10", + "handlebars": "^4.7.7" + }, + "engines": { + "node": ">=v12.22.9" + } + }, + "node_modules/express-handlebars/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/express-handlebars/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/express-handlebars/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/express-pouchdb": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/express-pouchdb/-/express-pouchdb-4.2.0.tgz", @@ -5832,8 +5911,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", @@ -6058,6 +6136,26 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -7687,6 +7785,11 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -7784,6 +7887,25 @@ "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.1.tgz", + "integrity": "sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemailer-express-handlebars": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nodemailer-express-handlebars/-/nodemailer-express-handlebars-6.0.0.tgz", + "integrity": "sha512-xo5nVCn2GDaE8o0ppOWVq0s3Tt5xLn+R2pDROQrZALKCoP6WsEOXrNGQwH/tYJ4vucWtWd+zdcHm734S6CSA7w==", + "dependencies": { + "express-handlebars": "^6.0.0" + }, + "engines": { + "node": "14.* || 16.* || >= 18" + } + }, "node_modules/noop-fn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/noop-fn/-/noop-fn-1.0.0.tgz", @@ -9996,7 +10118,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -10508,6 +10629,18 @@ "integrity": "sha512-LQc2s/ZDMaCN3QLpa+uzHUOQ7SdV0qgv3VBXOolQGXTaaZpIur6PwUclF5nN2hNkiTRcUugXd1zFOW3FLJ135Q==", "dev": true }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", diff --git a/package.json b/package.json index dc36598..ceec907 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@solidjs/testing-library": "^0.6.1", "@testing-library/jest-dom": "^5.16.5", "@types/lodash": "^4.14.191", + "@types/nodemailer": "^6.4.7", + "@types/nodemailer-express-handlebars": "^4.0.2", "@types/testing-library__jest-dom": "^5.14.5", "@types/uuid": "^9.0.1", "@vitest/coverage-c8": "^0.29.2", @@ -36,6 +38,8 @@ "@kobalte/core": "^0.6.2", "@types/pouchdb": "^6.4.0", "lodash": "^4.17.21", + "nodemailer": "^6.9.1", + "nodemailer-express-handlebars": "^6.0.0", "pouchdb": "^8.0.1", "pouchdb-server": "^4.2.0", "tippy.js": "^6.3.7", diff --git a/src/lib/get-url.ts b/src/lib/get-url.ts index 03d2f66..85276dd 100644 --- a/src/lib/get-url.ts +++ b/src/lib/get-url.ts @@ -1,7 +1,10 @@ import { adminCredentials } from '~/components/credentials'; -export const getUrl = (id: string, db = 'dyomedea_users') => { - const credentials = adminCredentials(); +export const getUrl = ( + id: string, + db = 'dyomedea_users', + credentials = adminCredentials() +) => { if (!credentials) { return ''; } diff --git a/src/lib/get.ts b/src/lib/get.ts index 88ee708..968ac9f 100644 --- a/src/lib/get.ts +++ b/src/lib/get.ts @@ -2,19 +2,24 @@ import { adminCredentials } from '~/components/credentials'; import { getUrl } from './get-url'; import { headersWithAuth } from './headers-with-auth'; -export const get = async (id: string, db = 'dyomedea_users') => { - const credentials = adminCredentials(); +export const get = async ( + id: string, + db = 'dyomedea_users', + credentials = adminCredentials() +) => { if (!credentials) { return null; } const { database } = credentials; - const headers = headersWithAuth(); + const headers = headersWithAuth(credentials); if (!headers) { return null; } - const response = await fetch(getUrl(id, db), { + // console.log({ caller: 'get', id, db, credentials, headers }); + + const response = await fetch(getUrl(id, db, credentials), { method: 'GET', mode: 'cors', headers, diff --git a/src/lib/obfuscate-mail.ts b/src/lib/obfuscate-mail.ts new file mode 100644 index 0000000..f9c9b68 --- /dev/null +++ b/src/lib/obfuscate-mail.ts @@ -0,0 +1,12 @@ +export const obfuscateMail = (mail: string) => { + const [user, hostname] = mail.split('@'); + const obfuscatedUser = + user.length < 4 + ? user[0] + new Array(user.length).join('.') + : user[0] + new Array(user.length - 1).join('.') + user[user.length - 1]; + const obfuscatedHostname = + hostname[0] + + new Array(hostname.length - 1).join('.') + + hostname[hostname.length - 1]; + return obfuscatedUser + '@' + obfuscatedHostname; +}; diff --git a/src/lib/put.ts b/src/lib/put.ts index 91ed785..db1b35b 100644 --- a/src/lib/put.ts +++ b/src/lib/put.ts @@ -8,36 +8,46 @@ export const put = async ( content: any, isNew: boolean = false, db = 'dyomedea_users', - overwrite_ = false + overwrite_ = false, + credentials = adminCredentials() ) => { - const credentials = adminCredentials(); + console.log({ caller: 'put', id, isNew, db, content, credentials }); + if (!credentials) { return null; } const { database } = credentials; if (!isNew) { - const previous = await get(id, db); + const previous = await get(id, db, credentials); + // console.log({ + // caller: 'put / after get', + // id, + // isNew, + // db, + // content, + // previous, + // }); if (!!previous) { content._rev = previous._rev; if (!overwrite_) { - content.__ = previous.__; + content.$ = previous.$; } } } - const headers = headersWithAuth(); + const headers = headersWithAuth(credentials); if (!headers) { return null; } headers.set('Content-type', 'application/json; charset=UTF-8'); - const response = await fetch(getUrl(id, db), { + const response = await fetch(getUrl(id, db, credentials), { method: 'PUT', mode: 'cors', headers, body: JSON.stringify(content), }); - console.log({ caller: 'put', id, isNew, db, status: response.status }); + // console.log({ caller: 'put', id, isNew, db, status: response.status }); return await response.json(); }; diff --git a/src/mail-template/email.handlebars b/src/mail-template/email.handlebars new file mode 100644 index 0000000..c7a20f7 --- /dev/null +++ b/src/mail-template/email.handlebars @@ -0,0 +1,14 @@ + + + + + + Validation + + +

Bonjour {{id}}!

+

Le code de validation de votre configuration pour l'application Dyomedea est {{code}}.

+

Ce code est valable pendant quinze minutes.

+

Eric

+ + \ No newline at end of file diff --git a/src/routes/api/conf/[token]/[code]/index.ts b/src/routes/api/conf/[token]/[code]/index.ts index abccdc4..4f5b402 100644 --- a/src/routes/api/conf/[token]/[code]/index.ts +++ b/src/routes/api/conf/[token]/[code]/index.ts @@ -1,6 +1,22 @@ import { APIEvent, json } from 'solid-start/api'; +import { findUserByToken } from '~/lib/find-user-by-token'; +import { readConfig } from '~/server-only-lib/read-config'; export async function GET({ params, env }: APIEvent) { - console.log({ caller: 'api/conf GET', params }); - return json({ params, env, response: 'OK' }); + const { credentials } = readConfig(); + const user = await findUserByToken(params.token, credentials); + console.log({ caller: 'api/conf GET', params, credentials, user }); + if (!user) { + return json({ params, response: 'NOT FOUND' }); + } + const { code, expiration } = user.$; + if ( + !code || + !expiration || + params.code !== code || + new Date() > new Date(expiration) + ) { + return json({ params, response: 'NOT FOUND' }); + } + return json({ params, response: 'OK', user }); } diff --git a/src/routes/api/conf/[token]/index.ts b/src/routes/api/conf/[token]/index.ts index eaa59f0..86383c9 100644 --- a/src/routes/api/conf/[token]/index.ts +++ b/src/routes/api/conf/[token]/index.ts @@ -1,11 +1,60 @@ import { APIEvent, json } from 'solid-start/api'; import { findUserByToken } from '~/lib/find-user-by-token'; import { readConfig } from '~/server-only-lib/read-config'; +import { createTransport } from 'nodemailer'; +import { resolve } from 'path'; +import hbs from 'nodemailer-express-handlebars'; +import { put } from '~/lib/put'; +import { obfuscateMail } from '~/lib/obfuscate-mail'; export async function GET({ params }: APIEvent) { - const { credentials } = readConfig(); + const { credentials, mailer } = readConfig(); console.log({ caller: 'api/conf GET', params, credentials }); const user = await findUserByToken(params.token, credentials); console.log({ caller: 'api/conf GET', params, credentials, user }); - return json({ params, response: 'OK' }); + if (!user) { + return json({ params, response: 'NOT FOUND' }); + } + const transporter = createTransport(mailer); + + const handlebarOptions = { + viewEngine: { + partialsDir: resolve('src/mail-template/'), + defaultLayout: false, + }, + viewPath: resolve('src/mail-template/'), + }; + transporter.use('compile', hbs(handlebarOptions)); + + const code = [...Array(6)].map((_) => (Math.random() * 10) | 0).join(''); + + const mailOptions = { + from: '"Dyomedea app" ', // sender address + to: user.mail, // list of receivers + subject: 'Validation!', + template: 'email', // the name of the template file i.e email.handlebars + context: { + id: user._id, + code, + }, + }; + + await transporter.sendMail(mailOptions); + + await put( + user._id, + { + ...user, + $: { + code, + expiration: new Date(Date.now() + 15 * 60 * 1000).toISOString(), + }, + }, + false, + undefined, + true, + credentials + ); + + return json({ params, status: 'OK', mail: obfuscateMail(user.mail) }); }