From 078b965733b6b1837d0c88958ff96e7efdef78dd Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sat, 14 Jan 2023 17:04:06 +0100 Subject: [PATCH] feat(backend): Reworked signup flow and translation --- .prettierignore | 1 + config/scripts.yaml | 1 + crowdin.yml | 2 + sites/backend/nodemon.json | 2 +- sites/backend/package.json | 3 +- sites/backend/public/locales/de/.gitkeep | 0 sites/backend/public/locales/en/.gitkeep | 0 sites/backend/public/locales/es/.gitkeep | 0 sites/backend/public/locales/fr/.gitkeep | 0 sites/backend/public/locales/nl/.gitkeep | 0 sites/backend/scripts/prebuild-i18n.mjs | 142 ++++++++++++++++++ sites/backend/scripts/prebuild.mjs | 30 +--- sites/backend/src/models/user.mjs | 54 ++++++- .../src/templates/email/emailchange.mjs | 73 --------- .../email/emailchange/emailchange.en.yaml | 6 + .../src/templates/email/emailchange/index.mjs | 19 +++ sites/backend/src/templates/email/goodbye.mjs | 41 ----- .../templates/email/goodbye/goodbye.en.yaml | 5 + .../src/templates/email/goodbye/index.mjs | 38 +++++ sites/backend/src/templates/email/index.mjs | 29 ++-- .../{loginlink.mjs => loginlink/index.mjs} | 23 +-- .../email/loginlink/loginlink.en.yaml | 5 + .../src/templates/email/newslettersub.mjs | 31 ---- .../templates/email/newslettersub/index.mjs | 24 +++ .../email/newslettersub/newslettersub.en.yaml | 5 + .../index.mjs} | 23 +-- .../email/passwordreset/passwordreset.en.yaml | 5 + .../templates/email/{ => shared}/blocks.mjs | 85 +++++------ .../src/templates/email/shared/shared.en.yaml | 9 ++ .../src/templates/email/signup-aea/index.mjs | 35 +++++ .../email/signup-aea/signup-aea.en.yaml | 7 + .../src/templates/email/signup-aed/index.mjs | 23 +++ .../email/signup-aed/signup-aed.en.yaml | 5 + sites/backend/src/templates/email/signup.mjs | 114 -------------- .../src/templates/email/signup/index.mjs | 32 ++++ .../src/templates/email/signup/signup.en.yaml | 6 + sites/backend/src/utils/email.mjs | 11 +- sites/backend/src/utils/index.mjs | 5 + sites/backend/tests/user.mjs | 8 +- 39 files changed, 522 insertions(+), 380 deletions(-) create mode 100644 sites/backend/public/locales/de/.gitkeep create mode 100644 sites/backend/public/locales/en/.gitkeep create mode 100644 sites/backend/public/locales/es/.gitkeep create mode 100644 sites/backend/public/locales/fr/.gitkeep create mode 100644 sites/backend/public/locales/nl/.gitkeep create mode 100644 sites/backend/scripts/prebuild-i18n.mjs delete mode 100644 sites/backend/src/templates/email/emailchange.mjs create mode 100644 sites/backend/src/templates/email/emailchange/emailchange.en.yaml create mode 100644 sites/backend/src/templates/email/emailchange/index.mjs delete mode 100644 sites/backend/src/templates/email/goodbye.mjs create mode 100644 sites/backend/src/templates/email/goodbye/goodbye.en.yaml create mode 100644 sites/backend/src/templates/email/goodbye/index.mjs rename sites/backend/src/templates/email/{loginlink.mjs => loginlink/index.mjs} (55%) create mode 100644 sites/backend/src/templates/email/loginlink/loginlink.en.yaml delete mode 100644 sites/backend/src/templates/email/newslettersub.mjs create mode 100644 sites/backend/src/templates/email/newslettersub/index.mjs create mode 100644 sites/backend/src/templates/email/newslettersub/newslettersub.en.yaml rename sites/backend/src/templates/email/{passwordreset.mjs => passwordreset/index.mjs} (60%) create mode 100644 sites/backend/src/templates/email/passwordreset/passwordreset.en.yaml rename sites/backend/src/templates/email/{ => shared}/blocks.mjs (81%) create mode 100644 sites/backend/src/templates/email/shared/shared.en.yaml create mode 100644 sites/backend/src/templates/email/signup-aea/index.mjs create mode 100644 sites/backend/src/templates/email/signup-aea/signup-aea.en.yaml create mode 100644 sites/backend/src/templates/email/signup-aed/index.mjs create mode 100644 sites/backend/src/templates/email/signup-aed/signup-aed.en.yaml delete mode 100644 sites/backend/src/templates/email/signup.mjs create mode 100644 sites/backend/src/templates/email/signup/index.mjs create mode 100644 sites/backend/src/templates/email/signup/signup.en.yaml diff --git a/.prettierignore b/.prettierignore index 8c7000943d4..a7f5f07fc2e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,3 +18,4 @@ yarn.lock .prettierignore .gitignore .eslintignore +.gitkeep diff --git a/config/scripts.yaml b/config/scripts.yaml index 31d52711065..e7cacf1bfca 100644 --- a/config/scripts.yaml +++ b/config/scripts.yaml @@ -58,6 +58,7 @@ backend: rmdb: 'node ./scripts/rmdb.mjs' test: 'npx mocha --require mocha-steps tests/index.mjs' vbuild: 'VERBOSE=1 node build.mjs' + prebuild: 'node scripts/prebuild.mjs' dev: build: &nextBuild 'node --experimental-json-modules ../../node_modules/next/dist/bin/next build' diff --git a/crowdin.yml b/crowdin.yml index 61def656176..bde414342da 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -12,3 +12,5 @@ files: translation: /sites/org/components/**/*.%two_letters_code%.yaml - source: /sites/shared/components/**/*.en.yaml translation: /sites/shared/components/**/*.%two_letters_code%.yaml + - source: /sites/backend/src/**/*.en.yaml + translation: /sites/backend/src/**/*.%two_letters_code%.yaml diff --git a/sites/backend/nodemon.json b/sites/backend/nodemon.json index 7d4f26382d0..21ec3c809e8 100644 --- a/sites/backend/nodemon.json +++ b/sites/backend/nodemon.json @@ -1,5 +1,5 @@ { "verbose": true, "ignore": ["tests/**.test.mjs"], - "watch": ["src/**", "openapi/*"] + "watch": ["src/**", "openapi/*", "public/locales/**"] } diff --git a/sites/backend/package.json b/sites/backend/package.json index a23d32aebe0..a4517763608 100644 --- a/sites/backend/package.json +++ b/sites/backend/package.json @@ -23,7 +23,8 @@ "prettier": "npx prettier --write 'src/*.mjs' 'tests/*.mjs'", "rmdb": "node ./scripts/rmdb.mjs", "test": "npx mocha --require mocha-steps tests/index.mjs", - "vbuild": "VERBOSE=1 node build.mjs" + "vbuild": "VERBOSE=1 node build.mjs", + "prebuild": "node scripts/prebuild.mjs" }, "peerDependencies": {}, "dependencies": { diff --git a/sites/backend/public/locales/de/.gitkeep b/sites/backend/public/locales/de/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sites/backend/public/locales/en/.gitkeep b/sites/backend/public/locales/en/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sites/backend/public/locales/es/.gitkeep b/sites/backend/public/locales/es/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sites/backend/public/locales/fr/.gitkeep b/sites/backend/public/locales/fr/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sites/backend/public/locales/nl/.gitkeep b/sites/backend/public/locales/nl/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sites/backend/scripts/prebuild-i18n.mjs b/sites/backend/scripts/prebuild-i18n.mjs new file mode 100644 index 00000000000..369f6b1a5d1 --- /dev/null +++ b/sites/backend/scripts/prebuild-i18n.mjs @@ -0,0 +1,142 @@ +import fs from 'fs' +import path from 'path' +import rdir from 'recursive-readdir' +import yaml from 'js-yaml' + +/* + * List of supported languages + */ +const locales = ['en', 'es', 'de', 'fr', 'nl'] + +/* + * This is where we configure what folders we should check for + * code-adjacent translation source files + */ +const folders = [path.resolve(path.join('.', 'src'))] + +/* + * Helper method to write out JSON files for translation sources + */ +const writeJson = async (locale, namespace, content) => + fs.writeFileSync( + path.resolve('.', 'public', 'locales', locale, `${namespace}.json`), + JSON.stringify(content) + ) + +/* + * Helper method to get a list of code-adjecent translation files in a folder. + * Will traverse recursively to get all files from a given root folder. + */ +const getI18nFileList = async () => { + const allFiles = [] + for (const dir of folders) { + try { + const dirFiles = await rdir(dir) + allFiles.push(...dirFiles) + } catch (err) { + console.log(err) + return false + } + } + + // Filter out the language files + return allFiles + .filter((file) => locales.map((loc) => `.${loc}.yaml`).includes(file.slice(-8))) + .sort() +} + +/* + * Helper method to get language and namespace from the filename + * + * Parameters: + * + * - filename: The filename or full path + filename + */ +const languageAndNamespaceFromFilename = (file) => { + const chunks = path.basename(file).split('.') + chunks.pop() + + return chunks +} + +/* + * Helper method to load a YAML file from disk + */ +const loadYaml = (file) => { + let data + try { + data = yaml.load(fs.readFileSync(file, 'utf-8')) + } catch (err) { + console.log(err) + } + + return data +} + +/* + * Helper method to build an object of namespaces and their values. + * Includes providing an EN fallback if something is not available in a language. + * + * Parameters: + * + * - files: List of files to process + */ +const filesAsNamespaces = (files) => { + // First build the object + const translations = {} + for (const file of files) { + const [namespace, lang] = languageAndNamespaceFromFilename(file) + if (typeof translations[namespace] === 'undefined') { + translations[namespace] = {} + } + translations[namespace][lang] = loadYaml(file) + } + + return translations +} + +/* + * Helper method to ensure all translations all available in the data + * + * Parameter: + * + * - data: The raw data based on loaded YAML files + */ +const fixData = (rawData) => { + const data = {} + for (const [namespace, nsdata] of Object.entries(rawData)) { + if (typeof nsdata.en === 'undefined') { + throw `No English data for namespace ${namespace}. Bailing out` + } + data[namespace] = { en: nsdata.en } + // Complete other locales + for (const lang of locales.filter((loc) => loc !== 'en')) { + if (typeof nsdata[lang] === 'undefined') data[namespace][lang] = nsdata.en + else { + for (const key of Object.keys(data[namespace].en)) { + if (typeof nsdata[lang][key] === 'undefined') nsdata[lang][key] = nsdata.en[key] + } + data[namespace][lang] = nsdata[lang] + } + } + } + + return data +} + +/* + * The method that does the actual work + */ +export const prebuildI18n = async () => { + // Handle new code-adjacent translations + const files = await getI18nFileList() + const data = filesAsNamespaces(files) + const namespaces = fixData(data) + // Write out code-adjacent source files + for (const locale of locales) { + // Fan out into namespaces + for (const namespace in namespaces) { + writeJson(locale, namespace, namespaces[namespace][locale]) + } + } +} diff --git a/sites/backend/scripts/prebuild.mjs b/sites/backend/scripts/prebuild.mjs index 7d875def645..72611282cd2 100644 --- a/sites/backend/scripts/prebuild.mjs +++ b/sites/backend/scripts/prebuild.mjs @@ -1,29 +1,3 @@ -import yaml from 'js-yaml' -import i18nConfig from '../next-i18next.config.js' -import fs from 'fs' -import path from 'path' +import { prebuildI18n } from './prebuild-i18n.mjs' -// This will load YAML translation files and store them as JSON -const generateTranslationFiles = async () => { - const promises = [] - for (const locale of i18nConfig.i18n.locales) { - for (const namespace of i18nConfig.namespaces) { - const content = yaml.load( - fs.readFileSync(path.resolve(path.join('locales', locale, namespace + '.yaml')), 'utf-8') - ) - console.log(`Generating ${locale}/${namespace}.json translation file`) - fs.writeFileSync( - path.resolve(path.join('public', 'locales', locale, namespace + '.json')), - JSON.stringify(content) - ) - } - } -} - -// Wrapper method -const prebuild = async () => { - await generateTranslationFiles() -} - -// Get to work -prebuild() +prebuildI18n() diff --git a/sites/backend/src/models/user.mjs b/sites/backend/src/models/user.mjs index 7c257f9fba7..9a7a55b0d13 100644 --- a/sites/backend/src/models/user.mjs +++ b/sites/backend/src/models/user.mjs @@ -2,7 +2,7 @@ import jwt from 'jsonwebtoken' import { log } from '../utils/log.mjs' import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs' import { setUserAvatar } from '../utils/sanity.mjs' -import { clean, asJson, i18nUrl } from '../utils/index.mjs' +import { clean, asJson, i18nUrl, capitalize } from '../utils/index.mjs' import { ConfirmationModel } from './confirmation.mjs' export function UserModel(tools) { @@ -148,8 +148,58 @@ UserModel.prototype.guardedCreate = async function ({ body }) { const ehash = hash(clean(body.email)) await this.read({ ehash }) - if (this.exists) return this.setResponse(400, 'emailExists') + if (this.exists) { + /* + * User already exists. However, if we return an error, then people can + * spam the signup endpoint to figure out who has a FreeSewing account + * which would be a privacy leak. So instead, pretend there is no user + * with that account, and that signup is proceeding as normal. + * Except that rather than a signup email, we send the user an info email. + * + * Note that we have to deal with 3 scenarios here: + * + * - Account exists, and is active (aea) + * - Account exists, but is inactive (regular signup) + * - Account exists, but is disabled (aed) + */ + // Set type of action based on the account status + let type = 'signup-aed' + if (this.record.status === 0) type = 'signup' + else if (this.record.status === 1) type = 'signup-aea' + // Create confirmation unless account is disabled + if (type !== 'signup-aed') { + this.confirmation = await this.Confirmation.create({ + type, + data: { + language: body.language, + email: this.clear.email, + id: this.record.id, + ehash: ehash, + }, + userId: this.record.id, + }) + } + // Always send email + await this.mailer.send({ + template: type, + language: body.language, + to: this.clear.email, + replacements: { + actionUrl: + type === 'signup-aed' + ? false // No actionUrl for disabled accounts + : i18nUrl(body.language, `/confirm/${type}/${this.Confirmation.record.id}`), + whyUrl: i18nUrl(body.language, `/docs/faq/email/why-${type}`), + supportUrl: i18nUrl(body.language, `/patrons/join`), + }, + }) + + // Now return as if everything is fine + return this.setResponse(201, false, { email: this.clear.email }) + } + + // New signup try { this.clear.email = clean(body.email) this.clear.initial = this.clear.email diff --git a/sites/backend/src/templates/email/emailchange.mjs b/sites/backend/src/templates/email/emailchange.mjs deleted file mode 100644 index 86bf2be3c32..00000000000 --- a/sites/backend/src/templates/email/emailchange.mjs +++ /dev/null @@ -1,73 +0,0 @@ -import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs' -import { translations as sharedTranslations } from './blocks.mjs' - -export const emailChange = { - html: wrap.html(` - ${headingRow.html} - ${lead1Row.html} - ${buttonRow.html} - ${closingRow.html} - `), - text: wrap.text(`${headingRow.text}${lead1Row.text}${buttonRow.text}${closingRow.text}`), -} - -export const translations = { - en: { - heading: 'Does this new E-mail address work?', - lead: 'To confirm your new E-mail address, click the big black rectangle below:', - button: 'Confirm E-mail change', - closing: "That's all it takes.", - greeting: 'love', - 'ps-pre-link': 'FreeSewing is free (duh), but please', - 'ps-link': 'become a patron', - 'ps-post-link': 'if you cxan afford it.', - ...sharedTranslations.en, - }, - // FIXME: Translate German - de: { - heading: 'Does this new E-mail address work?', - lead: 'To confirm your new E-mail address, click the big black rectangle below:', - button: 'Confirm E-mail change', - closing: "That's all it takes.", - greeting: 'love', - 'ps-pre-link': 'FreeSewing is free (duh), but please', - 'ps-link': 'become a patron', - 'ps-post-link': 'if you cxan afford it.', - ...sharedTranslations.de, - }, - // FIXME: Translate Spanish - es: { - heading: 'Does this new E-mail address work?', - lead: 'To confirm your new E-mail address, click the big black rectangle below:', - button: 'Confirm E-mail change', - closing: "That's all it takes.", - greeting: 'love', - 'ps-pre-link': 'FreeSewing is free (duh), but please', - 'ps-link': 'become a patron', - 'ps-post-link': 'if you cxan afford it.', - ...sharedTranslations.es, - }, - // FIXME: Translate French - fr: { - heading: 'Does this new E-mail address work?', - lead: 'To confirm your new E-mail address, click the big black rectangle below:', - button: 'Confirm E-mail change', - closing: "That's all it takes.", - greeting: 'love', - 'ps-pre-link': 'FreeSewing is free (duh), but please', - 'ps-link': 'become a patron', - 'ps-post-link': 'if you cxan afford it.', - ...sharedTranslations.fr, - }, - nl: { - heading: 'Werkt dit E-mail adres?', - lead: 'Om je E-mail wijziging te bevestigen moet je op de grote zwarte rechthoek hieronder te klikken:', - button: 'Bevestig je E-mail wijziging', - closing: 'Dat is al wat je moet doen.', - greeting: 'liefs', - 'ps-pre-link': 'FreeSewing is gratis (echt), maar gelieve', - 'ps-link': 'ons werk te ondersteunen', - 'ps-post-link': 'als het even kan.', - ...sharedTranslations.nl, - }, -} diff --git a/sites/backend/src/templates/email/emailchange/emailchange.en.yaml b/sites/backend/src/templates/email/emailchange/emailchange.en.yaml new file mode 100644 index 00000000000..ee7be3539ed --- /dev/null +++ b/sites/backend/src/templates/email/emailchange/emailchange.en.yaml @@ -0,0 +1,6 @@ +subject: Email change subject here FIXME +heading: Does this new E-mail address work? +lead: 'To confirm your new E-mail address, click the big black rectangle below:' +text-lead: 'To confirm your new E-mail address, click the link below:' +button: Confirm E-mail change +closing: That's all it takes. diff --git a/sites/backend/src/templates/email/emailchange/index.mjs b/sites/backend/src/templates/email/emailchange/index.mjs new file mode 100644 index 00000000000..1bacef59459 --- /dev/null +++ b/sites/backend/src/templates/email/emailchange/index.mjs @@ -0,0 +1,19 @@ +import { buttonRow, closingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs' +// Translations +import en from '../../../../public/locales/en/emailchange.json' assert { type: 'json' } +import de from '../../../../public/locales/de/emailchange.json' assert { type: 'json' } +import es from '../../../../public/locales/es/emailchange.json' assert { type: 'json' } +import fr from '../../../../public/locales/fr/emailchange.json' assert { type: 'json' } +import nl from '../../../../public/locales/nl/emailchange.json' assert { type: 'json' } + +export const emailChange = { + html: wrap.html(` + ${headingRow.html} + ${lead1Row.html} + ${buttonRow.html} + ${closingRow.html} + `), + text: wrap.text(`${headingRow.text}${lead1Row.text}${buttonRow.text}${closingRow.text}`), +} + +export const translations = { en, de, es, fr, nl } diff --git a/sites/backend/src/templates/email/goodbye.mjs b/sites/backend/src/templates/email/goodbye.mjs deleted file mode 100644 index 55a15a7c474..00000000000 --- a/sites/backend/src/templates/email/goodbye.mjs +++ /dev/null @@ -1,41 +0,0 @@ -import { headingRow, wrap } from './blocks.mjs' -import { translations as sharedTranslations } from './blocks.mjs' - -/* - * Used the following replacements: - * - heading - * - lead1 - * - lead2 - * - greeting - * - ps - */ -export const goodbye = { - html: wrap.html(` - ${headingRow.html} - - -

- {{ lead1 }} -
- {{ lead2 }} -

- {{ greeting }}, -
- joost -

- PS: {{ ps }}. -

- - -`), - text: wrap.text(`${headingRow.text} -{{lead1}} -{{lead2}} - -{{greeting}} -joost - -PS: {{ps}}`), -} - -export const translations = {} diff --git a/sites/backend/src/templates/email/goodbye/goodbye.en.yaml b/sites/backend/src/templates/email/goodbye/goodbye.en.yaml new file mode 100644 index 00000000000..c1bd9e918fe --- /dev/null +++ b/sites/backend/src/templates/email/goodbye/goodbye.en.yaml @@ -0,0 +1,5 @@ +subject: '[FreeSewing] Farewell' +heading: FIXME +lead: fixme +text-lead: fixme +closing: fixme diff --git a/sites/backend/src/templates/email/goodbye/index.mjs b/sites/backend/src/templates/email/goodbye/index.mjs new file mode 100644 index 00000000000..65734e78df0 --- /dev/null +++ b/sites/backend/src/templates/email/goodbye/index.mjs @@ -0,0 +1,38 @@ +import { headingRow, wrap } from '../shared/blocks.mjs' +// Translations +import en from '../../../../public/locales/en/goodbye.json' assert { type: 'json' } +import de from '../../../../public/locales/de/goodbye.json' assert { type: 'json' } +import es from '../../../../public/locales/es/goodbye.json' assert { type: 'json' } +import fr from '../../../../public/locales/fr/goodbye.json' assert { type: 'json' } +import nl from '../../../../public/locales/nl/goodbye.json' assert { type: 'json' } + +export const goodbye = { + html: wrap.html(` + ${headingRow.html} + + +

+ {{ lead1 }} +
+ {{ lead2 }} +

+ {{ greeting }}, +
+ joost +

+ PS: {{ ps }}. +

+ + +`), + text: wrap.text(`${headingRow.text} +{{lead1}} +{{lead2}} + +{{greeting}} +joost + +PS: {{ps}}`), +} + +export const translations = { en, de, es, fr, nl } diff --git a/sites/backend/src/templates/email/index.mjs b/sites/backend/src/templates/email/index.mjs index 591a09bfeaf..d504e76c8fc 100644 --- a/sites/backend/src/templates/email/index.mjs +++ b/sites/backend/src/templates/email/index.mjs @@ -1,9 +1,17 @@ -import { emailChange, translations as emailChangeTranslations } from './emailchange.mjs' -import { goodbye, translations as goodbyeTranslations } from './goodbye.mjs' -import { loginLink, translations as loginLinkTranslations } from './loginlink.mjs' -import { newsletterSub, translations as newsletterSubTranslations } from './newslettersub.mjs' -import { passwordReset, translations as passwordResetTranslations } from './passwordreset.mjs' -import { signup, translations as signupTranslations } from './signup.mjs' +import { emailChange, translations as emailChangeTranslations } from './emailchange/index.mjs' +import { goodbye, translations as goodbyeTranslations } from './goodbye/index.mjs' +import { loginLink, translations as loginLinkTranslations } from './loginlink/index.mjs' +import { newsletterSub, translations as newsletterSubTranslations } from './newslettersub/index.mjs' +import { passwordReset, translations as passwordResetTranslations } from './passwordreset/index.mjs' +import { signup, translations as signupTranslations } from './signup/index.mjs' +import { signupAea, translations as signupAeaTranslations } from './signup-aea/index.mjs' +import { signupAed, translations as signupAedTranslations } from './signup-aed/index.mjs' +// Shared translations +import en from '../../../public/locales/en/shared.json' assert { type: 'json' } +import de from '../../../public/locales/de/shared.json' assert { type: 'json' } +import es from '../../../public/locales/es/shared.json' assert { type: 'json' } +import fr from '../../../public/locales/fr/shared.json' assert { type: 'json' } +import nl from '../../../public/locales/nl/shared.json' assert { type: 'json' } export const templates = { emailChange, @@ -12,12 +20,10 @@ export const templates = { newsletterSub, passwordReset, signup, + 'signup-aea': signupAea, + 'signup-aed': signupAed, } -/* - * This is not part of our i18n package for... reasons - * It's not an accident, let's put it that way. - */ export const translations = { emailChange: emailChangeTranslations, goodbye: goodbyeTranslations, @@ -25,4 +31,7 @@ export const translations = { newsletterSub: newsletterSubTranslations, passwordReset: passwordResetTranslations, signup: signupTranslations, + 'signup-aea': signupAeaTranslations, + 'signup-aed': signupAedTranslations, + shared: { en, de, es, fr, nl }, } diff --git a/sites/backend/src/templates/email/loginlink.mjs b/sites/backend/src/templates/email/loginlink/index.mjs similarity index 55% rename from sites/backend/src/templates/email/loginlink.mjs rename to sites/backend/src/templates/email/loginlink/index.mjs index 68c224eea20..60617749dae 100644 --- a/sites/backend/src/templates/email/loginlink.mjs +++ b/sites/backend/src/templates/email/loginlink/index.mjs @@ -1,18 +1,11 @@ -import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs' -import { translations as sharedTranslations } from './blocks.mjs' +import { buttonRow, closingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs' +// Translations +import en from '../../../../public/locales/en/loginlink.json' assert { type: 'json' } +import de from '../../../../public/locales/de/loginlink.json' assert { type: 'json' } +import es from '../../../../public/locales/es/loginlink.json' assert { type: 'json' } +import fr from '../../../../public/locales/fr/loginlink.json' assert { type: 'json' } +import nl from '../../../../public/locales/nl/loginlink.json' assert { type: 'json' } -/* - * Used the following replacements: - * - actionUrl - * - heading - * - lead - * - button - * - closing - * - greeting - * - ps-pre-link - * - ps-link - * - ps-post-link - */ export const loginLink = { html: wrap.html(` ${headingRow} @@ -39,4 +32,4 @@ ${closingRow.text} `), } -export const translations = {} +export const translations = { en, de, es, fr, nl } diff --git a/sites/backend/src/templates/email/loginlink/loginlink.en.yaml b/sites/backend/src/templates/email/loginlink/loginlink.en.yaml new file mode 100644 index 00000000000..1eaadb62dae --- /dev/null +++ b/sites/backend/src/templates/email/loginlink/loginlink.en.yaml @@ -0,0 +1,5 @@ +subject: '[FreeSewing] Loginlink fixme' +heading: FIXME +lead: fixme +text-lead: fixme +closing: fixme diff --git a/sites/backend/src/templates/email/newslettersub.mjs b/sites/backend/src/templates/email/newslettersub.mjs deleted file mode 100644 index bc38dde0449..00000000000 --- a/sites/backend/src/templates/email/newslettersub.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs' -import { translations as sharedTranslations } from './blocks.mjs' - -/* - * Used the following replacements: - * - actionUrl - * - heading - * - lead - * - button - * - closing - * - greeting - * - ps-pre-link - * - ps-link - * - ps-post-link - */ -export const newsletterSub = { - html: wrap.html(` - ${headingRow.html} - ${lead1Row.html} - ${buttonRow.html} - ${closingRow.html} -`), - text: wrap.text(` -${headingRow.text} -${lead1Row.text} -${buttonRow.text} -${closingRow.text} -`), -} - -export const translations = {} diff --git a/sites/backend/src/templates/email/newslettersub/index.mjs b/sites/backend/src/templates/email/newslettersub/index.mjs new file mode 100644 index 00000000000..663ac7fe29a --- /dev/null +++ b/sites/backend/src/templates/email/newslettersub/index.mjs @@ -0,0 +1,24 @@ +import { buttonRow, closingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs' +// Translations +import en from '../../../../public/locales/en/newslettersub.json' assert { type: 'json' } +import de from '../../../../public/locales/de/newslettersub.json' assert { type: 'json' } +import es from '../../../../public/locales/es/newslettersub.json' assert { type: 'json' } +import fr from '../../../../public/locales/fr/newslettersub.json' assert { type: 'json' } +import nl from '../../../../public/locales/nl/newslettersub.json' assert { type: 'json' } + +export const newsletterSub = { + html: wrap.html(` + ${headingRow.html} + ${lead1Row.html} + ${buttonRow.html} + ${closingRow.html} +`), + text: wrap.text(` +${headingRow.text} +${lead1Row.text} +${buttonRow.text} +${closingRow.text} +`), +} + +export const translations = { en, de, es, fr, nl } diff --git a/sites/backend/src/templates/email/newslettersub/newslettersub.en.yaml b/sites/backend/src/templates/email/newslettersub/newslettersub.en.yaml new file mode 100644 index 00000000000..67cf759995e --- /dev/null +++ b/sites/backend/src/templates/email/newslettersub/newslettersub.en.yaml @@ -0,0 +1,5 @@ +subject: '[FreeSewing] newsletter sub fixme' +heading: FIXME +lead: fixme +text-lead: fixme +closing: fixme diff --git a/sites/backend/src/templates/email/passwordreset.mjs b/sites/backend/src/templates/email/passwordreset/index.mjs similarity index 60% rename from sites/backend/src/templates/email/passwordreset.mjs rename to sites/backend/src/templates/email/passwordreset/index.mjs index 2722cfc8ebc..458b1d8ec9d 100644 --- a/sites/backend/src/templates/email/passwordreset.mjs +++ b/sites/backend/src/templates/email/passwordreset/index.mjs @@ -1,18 +1,11 @@ -import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs' -import { translations as sharedTranslations } from './blocks.mjs' +import { buttonRow, closingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs' +// Translations +import en from '../../../../public/locales/en/passwordreset.json' assert { type: 'json' } +import de from '../../../../public/locales/de/passwordreset.json' assert { type: 'json' } +import es from '../../../../public/locales/es/passwordreset.json' assert { type: 'json' } +import fr from '../../../../public/locales/fr/passwordreset.json' assert { type: 'json' } +import nl from '../../../../public/locales/nl/passwordreset.json' assert { type: 'json' } -/* - * Used the following replacements: - * - actionUrl - * - heading - * - lead - * - button - * - closing - * - greeting - * - ps-pre-link - * - ps-link - * - ps-post-link - */ export const passwordReset = { html: wrap.html(` ${headingRow.html} @@ -41,4 +34,4 @@ ${closingRow.text} `), } -export const translations = {} +export const translations = { en, de, es, fr, nl } diff --git a/sites/backend/src/templates/email/passwordreset/passwordreset.en.yaml b/sites/backend/src/templates/email/passwordreset/passwordreset.en.yaml new file mode 100644 index 00000000000..f4ad0c51f2d --- /dev/null +++ b/sites/backend/src/templates/email/passwordreset/passwordreset.en.yaml @@ -0,0 +1,5 @@ +subject: '[FreeSewing] passwordreset fixme' +heading: FIXME +lead: fixme +text-lead: fixme +closing: fixme diff --git a/sites/backend/src/templates/email/blocks.mjs b/sites/backend/src/templates/email/shared/blocks.mjs similarity index 81% rename from sites/backend/src/templates/email/blocks.mjs rename to sites/backend/src/templates/email/shared/blocks.mjs index 54a49df4341..d8de4ea55aa 100644 --- a/sites/backend/src/templates/email/blocks.mjs +++ b/sites/backend/src/templates/email/shared/blocks.mjs @@ -25,12 +25,10 @@ export const closingRow = {
joost

- - PS: {{ ps-pre-link}} - - {{ ps-link}} - {{ ps-post-link }} - + PS: {{ ps-pre-link}} + + {{ ps-link}} + {{ ps-post-link }}

`, @@ -75,6 +73,25 @@ export const lead1Row = { `, } +export const preLeadRow = { + html: ` + + +

+ {{ preLead }} +

+ + {{ lead }} + +

+ + `, + text: `{{{ preLead }}} + {{{ text-lead }}} + {{{ actionUrl }}} + `, +} + export const wrap = { html: (body) => ` @@ -185,23 +202,26 @@ export const wrap = { @@ -223,8 +243,8 @@ ${body} -- FreeSewing -Plantin en Moretuslei 69 -Antwerp +Plantin en Moretuslei 67 +Antwerp 2018 Belgium {{ website }} : {{{ urlWebsite }}} @@ -234,26 +254,3 @@ Twitter : https://twitter.com/freesewing_org {{ whyDidIGetThis }} : {{{ whyUrl }}} `, } - -export const translations = { - en: { - whyDidIGetThis: 'Why did I get this email?', - website: 'freesewing.org', - }, - de: { - whyDidIGetThis: 'Why did I get this?', // FIXME: Provide German translation - website: 'freesewing.org/de', - }, - es: { - whyDidIGetThis: 'Why did I get this?', // FIXME: Provide Spanish translation - website: 'freesewing.org/es', - }, - fr: { - whyDidIGetThis: 'Why did I get this?', // FIXME: Provide French translation - website: 'freesewing.org/fr', - }, - nl: { - whyDidIGetThis: 'Waarom kreeg ik deze email?', - website: 'freesewing.org/nl', - }, -} diff --git a/sites/backend/src/templates/email/shared/shared.en.yaml b/sites/backend/src/templates/email/shared/shared.en.yaml new file mode 100644 index 00000000000..e20a59187dd --- /dev/null +++ b/sites/backend/src/templates/email/shared/shared.en.yaml @@ -0,0 +1,9 @@ +# Shared +greeting: love +ps-pre-link: FreeSewing is free (duh), but please +ps-link: become a patron +ps-post-link: if you can afford it. +text-ps: 'FreeSewing is free (duh), but please become a patron if you can afford it.' +notMarketing: This is not marketing, but a transactional email about your FreeSewing account. +seeWhy: 'For more info, see:' +whyDidIGetThis: Why did I get this email? diff --git a/sites/backend/src/templates/email/signup-aea/index.mjs b/sites/backend/src/templates/email/signup-aea/index.mjs new file mode 100644 index 00000000000..bce323c2933 --- /dev/null +++ b/sites/backend/src/templates/email/signup-aea/index.mjs @@ -0,0 +1,35 @@ +import { buttonRow, closingRow, headingRow, preLeadRow, wrap } from '../shared/blocks.mjs' +// Translations +import en from '../../../../public/locales/en/signup-aea.json' assert { type: 'json' } +import de from '../../../../public/locales/de/signup-aea.json' assert { type: 'json' } +import es from '../../../../public/locales/es/signup-aea.json' assert { type: 'json' } +import fr from '../../../../public/locales/fr/signup-aea.json' assert { type: 'json' } +import nl from '../../../../public/locales/nl/signup-aea.json' assert { type: 'json' } + +// aea = Account Exists and is Active +export const signupAea = { + html: wrap.html(` + ${headingRow.html} + ${preLeadRow.html} + ${buttonRow.html} + ${closingRow.html} +`), + text: wrap.text(` +{{{ heading }}} + +{{{ preLead }}} + +{{{ textLead }}} + +{{{ actionUrl }}} + +{{{ closing }}} + +{{{ greeting }}}, +joost + +PS: {{{ text-ps }}} : {{{ supportUrl }}} +`), +} + +export const translations = { en, de, es, fr, nl } diff --git a/sites/backend/src/templates/email/signup-aea/signup-aea.en.yaml b/sites/backend/src/templates/email/signup-aea/signup-aea.en.yaml new file mode 100644 index 00000000000..edff52d6da4 --- /dev/null +++ b/sites/backend/src/templates/email/signup-aea/signup-aea.en.yaml @@ -0,0 +1,7 @@ +subject: "[FreeSewing] No need to sign up, you're already in" +heading: Welcome back to FreeSewing +preLead: 'Someone (you?) tried to sign up with this email address. But we already have an active account tied to this email.' +lead: 'To log in to your account, click the big black button below:' +text-lead: 'To log in to your account, click the link below:' +button: Log in +closing: "That's all it takes." diff --git a/sites/backend/src/templates/email/signup-aed/index.mjs b/sites/backend/src/templates/email/signup-aed/index.mjs new file mode 100644 index 00000000000..95f126f2817 --- /dev/null +++ b/sites/backend/src/templates/email/signup-aed/index.mjs @@ -0,0 +1,23 @@ +import { closingRow, headingRow, preLeadRow, wrap } from '../shared/blocks.mjs' +// Translations +import en from '../../../../public/locales/en/signup-aed.json' assert { type: 'json' } +import de from '../../../../public/locales/de/signup-aed.json' assert { type: 'json' } +import es from '../../../../public/locales/es/signup-aed.json' assert { type: 'json' } +import fr from '../../../../public/locales/fr/signup-aed.json' assert { type: 'json' } +import nl from '../../../../public/locales/nl/signup-aed.json' assert { type: 'json' } + +// aed = Account Exists but is Disabled +export const signupAed = { + html: wrap.html(` + ${headingRow.html} + ${preLeadRow.html} + ${closingRow.html} +`), + text: wrap.text(`${headingRow.text} +{{ prelead }} +{{lead }} +${closingRow.text} +`), +} + +export const translations = { en, de, es, fr, nl } diff --git a/sites/backend/src/templates/email/signup-aed/signup-aed.en.yaml b/sites/backend/src/templates/email/signup-aed/signup-aed.en.yaml new file mode 100644 index 00000000000..1348808394a --- /dev/null +++ b/sites/backend/src/templates/email/signup-aed/signup-aed.en.yaml @@ -0,0 +1,5 @@ +subject: '[FreeSewing] Your account is marked as disabled' +heading: Your FreeSewing account is disabled +preLead: 'An account can become disabled when a user revokes consent, or (exceptionally) when an administrator disables it.' +lead: 'In any case, the only way to re-enable a disabled account is to reach out to support.' +closing: 'To contact support, you can reply to this email.' diff --git a/sites/backend/src/templates/email/signup.mjs b/sites/backend/src/templates/email/signup.mjs deleted file mode 100644 index d6926680e35..00000000000 --- a/sites/backend/src/templates/email/signup.mjs +++ /dev/null @@ -1,114 +0,0 @@ -import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs' -import { translations as sharedTranslations } from './blocks.mjs' - -/* - * Used the following replacements: - * - actionUrl - * - heading - * - lead - * - button - * - closing - * - greeting - * - ps-pre-link - * - ps-link - * - ps-post-link - */ -export const signup = { - html: wrap.html(` - ${headingRow.html} - ${lead1Row.html} - ${buttonRow.html} - ${closingRow.html} -`), - text: wrap.text(` -{{{ heading }}} - -{{{ textLead }}} - -{{{ actionUrl }}} - -{{{ closing }}} - -{{{ greeting }}}, -joost - -PS: {{{ text-ps }}} : {{{ supportUrl }}} -`), -} - -export const translations = { - en: { - subject: '[FreeSewing] Confirm your E-mail address to activate your account', - heading: 'Welcome to FreeSewing', - lead: 'To activate your account, click the big black rectangle below:', - textLead: 'To activate your account, click the link below:', - button: 'Activate account', - closing: "That's all for now.", - greeting: 'love', - 'ps-pre-link': 'FreeSewing is free (duh), but please', - 'ps-link': 'become a patron', - 'ps-post-link': 'if you cxan afford it.', - 'text-ps': 'FreeSewing is free (duh), but please become a patron if you can afford it', - ...sharedTranslations.en, - }, - // FIXME: Translate German - de: { - subject: '[FreeSewing] Confirm your E-mail address to activate your account', - heading: 'Welcome to FreeSewing', - lead: 'To activate your account, click the big black rectangle below:', - textLead: 'To activate your account, click the link below:', - button: 'Activate account', - closing: "That's all for now.", - greeting: 'love', - 'ps-pre-link': 'FreeSewing is free (duh), but please', - 'ps-link': 'become a patron', - 'ps-post-link': 'if you can afford it.', - 'text-ps': 'FreeSewing is free (duh), but please become a patron if you can afford it', - ...sharedTranslations.de, - }, - // FIXME: Translate Spanish - es: { - subject: '[FreeSewing] Confirm your E-mail address to activate your account', - heading: 'Welcome to FreeSewing', - lead: 'To activate your account, click the big black rectangle below:', - textLead: 'To activate your account, click the link below:', - button: 'Activate account', - closing: "That's all for now.", - greeting: 'love', - 'ps-pre-link': 'FreeSewing is free (duh), but please', - 'ps-link': 'become a patron', - 'ps-post-link': 'if you can afford it.', - 'text-ps': 'FreeSewing is free (duh), but please become a patron if you can afford it', - ...sharedTranslations.es, - }, - // FIXME: Translate French - fr: { - subject: '[FreeSewing] Confirm your E-mail address to activate your account', - heading: 'Welcome to FreeSewing', - lead: 'To activate your account, click the big black rectangle below:', - textLead: 'To activate your account, click the link below:', - button: 'Activate account', - closing: "That's all for now.", - greeting: 'love', - 'ps-pre-link': 'FreeSewing is free (duh), but please', - 'ps-link': 'become a patron', - 'ps-post-link': 'if you can afford it.', - 'text-ps': 'FreeSewing is free (duh), but please become a patron if you can afford it', - ...sharedTranslations.fr, - }, - nl: { - subject: '[FreeSewing] Bevestig je E-mail adres om je account te activeren', - heading: 'Welkom bij FreeSewing', - lead: 'Om je account te activeren moet je op de grote zwarte rechthoek hieronder te klikken:', - textLead: 'Om je account te activeren moet je op de link hieronder te klikken:', - button: 'Account activeren', - closing: 'Daarmee is dat ook weer geregeld.', - greeting: 'liefs', - 'ps-pre-link': 'FreeSewing is gratis (echt), maar gelieve', - 'ps-link': 'ons werk te ondersteunen', - 'ps-post-link': 'als het even kan.', - 'text-ps': - 'FreeSewing is gratis (echt), maar gelieve ons werk te ondersteunen als het even kan', - ...sharedTranslations.nl, - }, -} diff --git a/sites/backend/src/templates/email/signup/index.mjs b/sites/backend/src/templates/email/signup/index.mjs new file mode 100644 index 00000000000..43a7a2801c4 --- /dev/null +++ b/sites/backend/src/templates/email/signup/index.mjs @@ -0,0 +1,32 @@ +import { buttonRow, closingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs' +// Translations +import en from '../../../../public/locales/en/signup.json' assert { type: 'json' } +import de from '../../../../public/locales/de/signup.json' assert { type: 'json' } +import es from '../../../../public/locales/es/signup.json' assert { type: 'json' } +import fr from '../../../../public/locales/fr/signup.json' assert { type: 'json' } +import nl from '../../../../public/locales/nl/signup.json' assert { type: 'json' } + +export const signup = { + html: wrap.html(` + ${headingRow.html} + ${lead1Row.html} + ${buttonRow.html} + ${closingRow.html} +`), + text: wrap.text(` +{{{ heading }}} + +{{{ textLead }}} + +{{{ actionUrl }}} + +{{{ closing }}} + +{{{ greeting }}}, +joost + +PS: {{{ text-ps }}} : {{{ supportUrl }}} +`), +} + +export const translations = { en, de, es, fr, nl } diff --git a/sites/backend/src/templates/email/signup/signup.en.yaml b/sites/backend/src/templates/email/signup/signup.en.yaml new file mode 100644 index 00000000000..f821ca52d91 --- /dev/null +++ b/sites/backend/src/templates/email/signup/signup.en.yaml @@ -0,0 +1,6 @@ +subject: "[FreeSewing] Here's that signup link we promised you" +heading: Join FreeSewing +lead: 'To create a FreeSewing account linked to this email address, click the big black rectangle below:' +text-lead: 'To create a FreeSewing account linked to this email address, click the link below:' +button: Create an account +closing: "That's all for now." diff --git a/sites/backend/src/utils/email.mjs b/sites/backend/src/utils/email.mjs index f54d4adf136..cde3071480a 100644 --- a/sites/backend/src/utils/email.mjs +++ b/sites/backend/src/utils/email.mjs @@ -1,6 +1,7 @@ import { templates, translations } from '../templates/email/index.mjs' import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2' import mustache from 'mustache' +import { log } from './log.mjs' /* * Exporting this closure that makes sure we have access to the @@ -20,14 +21,22 @@ export const mailer = (config) => ({ */ async function sendEmailViaAwsSes(config, { template, to, language = 'en', replacements = {} }) { // Make sure we have what it takes - if (!template || !to || typeof templates[template] === 'undefined') return false + if (!template || !to || typeof templates[template] === 'undefined') { + log.warn(`Tried to email invalid template: ${template}`) + return false + } + + log.info(`Emailing template ${template} to ${to}`) // Load template const { html, text } = templates[template] const replace = { + website: `FreeSewing.org`, + ...translations.shared[language], ...translations[template][language], ...replacements, } + if (language !== 'en') replace.website += `${language}/` // IMHO the AWS apis are a complete clusterfuck const client = new SESv2Client({ region: config.aws.ses.region }) diff --git a/sites/backend/src/utils/index.mjs b/sites/backend/src/utils/index.mjs index 7c8d7abacc1..2a0f7ac1f63 100644 --- a/sites/backend/src/utils/index.mjs +++ b/sites/backend/src/utils/index.mjs @@ -25,3 +25,8 @@ export const i18nUrl = (lang, path) => { return url + path } + +/* + * Capitalizes a string + */ +export const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1) diff --git a/sites/backend/tests/user.mjs b/sites/backend/tests/user.mjs index b4acc5003e8..095e1a179ae 100644 --- a/sites/backend/tests/user.mjs +++ b/sites/backend/tests/user.mjs @@ -42,17 +42,17 @@ export const userTests = async (chai, config, expect, store) => { }) }) - step(`${store.icon('user')} Should fail to signup an existing email address`, (done) => { + step(`${store.icon('user')} Should pretend to signup an existing email address`, (done) => { chai .request(config.api) .post('/signup') .send(fields) .end((err, res) => { - expect(res.status).to.equal(400) + expect(res.status).to.equal(201) expect(res.type).to.equal('application/json') expect(res.charset).to.equal('utf-8') - expect(res.body.result).to.equal(`error`) - expect(res.body.error).to.equal('emailExists') + expect(res.body.result).to.equal(`success`) + expect(res.body.email).to.equal(fields.email) done() }) })