feat(backend): Reworked signup flow and translation
This commit is contained in:
parent
7ceeeffcfc
commit
078b965733
39 changed files with 522 additions and 380 deletions
|
@ -18,3 +18,4 @@ yarn.lock
|
|||
.prettierignore
|
||||
.gitignore
|
||||
.eslintignore
|
||||
.gitkeep
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"verbose": true,
|
||||
"ignore": ["tests/**.test.mjs"],
|
||||
"watch": ["src/**", "openapi/*"]
|
||||
"watch": ["src/**", "openapi/*", "public/locales/**"]
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
0
sites/backend/public/locales/de/.gitkeep
Normal file
0
sites/backend/public/locales/de/.gitkeep
Normal file
0
sites/backend/public/locales/en/.gitkeep
Normal file
0
sites/backend/public/locales/en/.gitkeep
Normal file
0
sites/backend/public/locales/es/.gitkeep
Normal file
0
sites/backend/public/locales/es/.gitkeep
Normal file
0
sites/backend/public/locales/fr/.gitkeep
Normal file
0
sites/backend/public/locales/fr/.gitkeep
Normal file
0
sites/backend/public/locales/nl/.gitkeep
Normal file
0
sites/backend/public/locales/nl/.gitkeep
Normal file
142
sites/backend/scripts/prebuild-i18n.mjs
Normal file
142
sites/backend/scripts/prebuild-i18n.mjs
Normal 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])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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.
|
19
sites/backend/src/templates/email/emailchange/index.mjs
Normal file
19
sites/backend/src/templates/email/emailchange/index.mjs
Normal 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 }
|
|
@ -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 = {}
|
|
@ -0,0 +1,5 @@
|
|||
subject: '[FreeSewing] Farewell'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
38
sites/backend/src/templates/email/goodbye/index.mjs
Normal file
38
sites/backend/src/templates/email/goodbye/index.mjs
Normal 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 }
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -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 }
|
|
@ -0,0 +1,5 @@
|
|||
subject: '[FreeSewing] Loginlink fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -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 = {}
|
24
sites/backend/src/templates/email/newslettersub/index.mjs
Normal file
24
sites/backend/src/templates/email/newslettersub/index.mjs
Normal 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 }
|
|
@ -0,0 +1,5 @@
|
|||
subject: '[FreeSewing] newsletter sub fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -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 }
|
|
@ -0,0 +1,5 @@
|
|||
subject: '[FreeSewing] passwordreset fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -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"> | </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"> | </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"> | </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"> | </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 }}
|
||||
<a href="{{ urlWhy }}" target="_blank" style="text-decoration: underline; color: #868e96">{{ whyDidIGetThis }}</a>
|
||||
<br>
|
||||
<br>
|
||||
FreeSewing
|
||||
<span style="font-size: 13px; color: #737373"> - </span>
|
||||
Plantin en Moretuslei 69
|
||||
<span style="font-size: 13px; color: #737373"> - </span>
|
||||
Antwerp
|
||||
<span style="font-size: 13px; color: #737373"> - </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',
|
||||
},
|
||||
}
|
9
sites/backend/src/templates/email/shared/shared.en.yaml
Normal file
9
sites/backend/src/templates/email/shared/shared.en.yaml
Normal 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?
|
35
sites/backend/src/templates/email/signup-aea/index.mjs
Normal file
35
sites/backend/src/templates/email/signup-aea/index.mjs
Normal 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 }
|
|
@ -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."
|
23
sites/backend/src/templates/email/signup-aed/index.mjs
Normal file
23
sites/backend/src/templates/email/signup-aed/index.mjs
Normal 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 }
|
|
@ -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.'
|
|
@ -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,
|
||||
},
|
||||
}
|
32
sites/backend/src/templates/email/signup/index.mjs
Normal file
32
sites/backend/src/templates/email/signup/index.mjs
Normal 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 }
|
6
sites/backend/src/templates/email/signup/signup.en.yaml
Normal file
6
sites/backend/src/templates/email/signup/signup.en.yaml
Normal 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."
|
|
@ -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 })
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue