1
0
Fork 0

feat(backend): Reworked signup flow and translation

This commit is contained in:
joostdecock 2023-01-14 17:04:06 +01:00
parent 7ceeeffcfc
commit 078b965733
39 changed files with 522 additions and 380 deletions

View file

@ -18,3 +18,4 @@ yarn.lock
.prettierignore
.gitignore
.eslintignore
.gitkeep

View file

@ -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'

View file

@ -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

View file

@ -1,5 +1,5 @@
{
"verbose": true,
"ignore": ["tests/**.test.mjs"],
"watch": ["src/**", "openapi/*"]
"watch": ["src/**", "openapi/*", "public/locales/**"]
}

View file

@ -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": {

View file

View file

View file

View file

View file

View file

@ -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])
}
}
}

View file

@ -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()

View file

@ -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

View file

@ -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,
},
}

View file

@ -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.

View file

@ -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 }

View file

@ -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}
<tr>
<td align="left" class="sm-p-15px" style="padding-top: 15px">
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #262626">
{{ lead1 }}
<br>
{{ lead2 }}
<br><br>
{{ greeting }},
<br>
joost
<br><br>
<small>PS: {{ ps }}.</small>
</p>
</td>
</tr>
`),
text: wrap.text(`${headingRow.text}
{{lead1}}
{{lead2}}
{{greeting}}
joost
PS: {{ps}}`),
}
export const translations = {}

View file

@ -0,0 +1,5 @@
subject: '[FreeSewing] Farewell'
heading: FIXME
lead: fixme
text-lead: fixme
closing: fixme

View file

@ -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}
<tr>
<td align="left" class="sm-p-15px" style="padding-top: 15px">
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #262626">
{{ lead1 }}
<br>
{{ lead2 }}
<br><br>
{{ greeting }},
<br>
joost
<br><br>
<small>PS: {{ ps }}.</small>
</p>
</td>
</tr>
`),
text: wrap.text(`${headingRow.text}
{{lead1}}
{{lead2}}
{{greeting}}
joost
PS: {{ps}}`),
}
export const translations = { en, de, es, fr, nl }

View file

@ -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 },
}

View file

@ -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 }

View file

@ -0,0 +1,5 @@
subject: '[FreeSewing] Loginlink fixme'
heading: FIXME
lead: fixme
text-lead: fixme
closing: fixme

View file

@ -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 = {}

View file

@ -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 }

View file

@ -0,0 +1,5 @@
subject: '[FreeSewing] newsletter sub fixme'
heading: FIXME
lead: fixme
text-lead: fixme
closing: fixme

View file

@ -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 }

View file

@ -0,0 +1,5 @@
subject: '[FreeSewing] passwordreset fixme'
heading: FIXME
lead: fixme
text-lead: fixme
closing: fixme

View file

@ -25,12 +25,10 @@ export const closingRow = {
<br>
joost
<br><br>
<small>
PS: {{ ps-pre-link}}
<a href="{{ supportUrl }}" target="_blank" style="text-decoration: none; color: #262626">
<b>{{ ps-link}}</b>
</a> {{ ps-post-link }}
</small>
PS: {{ ps-pre-link}}
<a href="{{ supportUrl }}" target="_blank" style="text-decoration: underline; color: #262626">
<b>{{ ps-link}}</b>
</a> {{ ps-post-link }}
</p>
</td>
</tr>`,
@ -75,6 +73,25 @@ export const lead1Row = {
`,
}
export const preLeadRow = {
html: `
<tr>
<td align="left" class="sm-p-15px" style="padding-top: 15px">
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #262626">
{{ preLead }}
<br><br>
<a href="{{ actionUrl }}" target="_blank" style="text-decoration: none; color: #262626">
<b>{{ lead }}</b>
</a>
</p>
</td>
</tr>`,
text: `{{{ preLead }}}
{{{ text-lead }}}
{{{ actionUrl }}}
`,
}
export const wrap = {
html: (body) => `<!DOCTYPE html>
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
@ -185,23 +202,26 @@ export const wrap = {
<table align="center" class="sm-max-w-full" style="width: 100%; max-width: 500px" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="left" style="border-top: 1px solid #ddd; padding: 8px" ;>
<p style="margin: 0; font-size: 14px; line-height: 24px; color: #a3a3a3">
<a href="{{ urlWebsite }}" target="_blank" style="text-decoration: none; color: #a3a3a3"><b>{{ website }}</b></a>
<p style="margin: 0; font-size: 14px; line-height: 24px; color: #868e96; text-align: center;">
<a href="https://{{ website }}" target="_blank" style="text-decoration: underline; color: #868e96"><b>{{ website }}</b></a>
<span style="font-size: 13px; color: #737373">&nbsp;&nbsp;|&nbsp;&nbsp;</span>
<a href="https://github.com/fresewing/freesewing" target="_blank" style="text-decoration: none; color: #a3a3a3"><b>Github</b></a>
<a href="https://github.com/fresewing/freesewing" target="_blank" style="text-decoration: underline; color: #868e96"><b>Github</b></a>
<span style="font-size: 13px; color: #737373">&nbsp;&nbsp;|&nbsp;&nbsp;</span>
<a href="https://discord.freesewing.org/" target="_blank" style="text-decoration: none; color: #a3a3a3"><b>Discord</b></a>
<span style="font-size: 13px; color: #737373">&nbsp;&nbsp;|&nbsp;&nbsp;</span>
<a href="https://twitter.com/freesewing_org" target="_blank" style="text-decoration: none; color: #a3a3a3"><b>Twitter</b></a>
<span style="font-size: 13px; color: #737373">&nbsp;&nbsp;|&nbsp;&nbsp;</span>
<a href="{{ urlWhy }}" target="_blank" style="text-decoration: none; color: #a3a3a3"><b>{{ whyDidIGetThis }}</b></a>
<a href="https://discord.freesewing.org/" target="_blank" style="text-decoration: underline; color: #868e96"><b>Discord</b></a>
</p>
<p style="margin: 0; font-size: 12px; margin-top: 12px; line-height: 18px; color: #868e96; text-align: center;">
{{ notMarketing }}
<br>
{{ seeWhy }}&nbsp;
<a href="{{ urlWhy }}" target="_blank" style="text-decoration: underline; color: #868e96">{{ whyDidIGetThis }}</a>
<br>
<br>
FreeSewing
<span style="font-size: 13px; color: #737373">&nbsp;&nbsp;-&nbsp;&nbsp;</span>
Plantin en Moretuslei 69
<span style="font-size: 13px; color: #737373">&nbsp;&nbsp;-&nbsp;&nbsp;</span>
Antwerp
<span style="font-size: 13px; color: #737373">&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<br>
67 Plantin en Moretuslei
<br>
Antwerp 2018
<br>
Belgium
</p>
</td>
@ -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',
},
}

View file

@ -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?

View file

@ -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 }

View file

@ -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."

View file

@ -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 }

View file

@ -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.'

View file

@ -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,
},
}

View file

@ -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 }

View file

@ -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."

View file

@ -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 })

View file

@ -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)

View file

@ -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()
})
})