feat(backend): Added newsletter subscriptions
This commit is contained in:
parent
9460d98f6a
commit
2210f26e03
176 changed files with 1728 additions and 629 deletions
|
@ -61,7 +61,6 @@ backend:
|
|||
rmdb: 'node ./scripts/rmdb.mjs'
|
||||
test: 'npx mocha --require mocha-steps tests/index.mjs'
|
||||
vbuild: *vbuild
|
||||
prebuild: &sitePrebuild 'node --conditions=internal --experimental-json-modules ./prebuild.mjs'
|
||||
|
||||
dev:
|
||||
build: &nextBuild 'next build'
|
||||
|
@ -71,7 +70,7 @@ dev:
|
|||
develop: *nextDev
|
||||
i18n: "SITE=dev node --conditions=internal ../shared/prebuild/i18n-only.mjs"
|
||||
lint: &nextLint 'next lint'
|
||||
prebuild: *sitePrebuild
|
||||
prebuild: &sitePrebuild 'node --conditions=internal --experimental-json-modules ./prebuild.mjs'
|
||||
serve: "pm2 start npm --name 'dev' -- run start"
|
||||
start: &nextStart 'yarn prebuild && yarn dev'
|
||||
|
||||
|
|
5
markdown/org/docs/faq/newsletter/en.md
Normal file
5
markdown/org/docs/faq/newsletter/en.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Newsletter
|
||||
---
|
||||
|
||||
<ReadMore />
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
title: Why do I have to click again to confirm I want to subscribe after I already clicked the confirmation link you sent me?
|
||||
---
|
||||
|
||||
There are two aspects that cause signing up for our newsletter to require multiple clicks:
|
||||
|
||||
- [People should only be able to sign up themselves](#people-should-only-be-able-to-sign-up-themselves)
|
||||
- [GET requests should not make changes](#get-requests-should-not-make-changes)
|
||||
|
||||
## People should only be able to sign up themselves
|
||||
|
||||
This one is pretty easy to understand. One should not be able
|
||||
to subscribe somebody else's email address to the FreeSewing newsletter.
|
||||
|
||||
This is why, after indicating you want to sign up, we sent you a confirmation
|
||||
email to the email address you provided.
|
||||
If you receive this email, it confirms not only that the email address
|
||||
is working, but also that you have access to it.
|
||||
|
||||
In other words, only after you click the link in the confirmation email can we know for
|
||||
cerntain that:
|
||||
|
||||
- The email address is valid
|
||||
- The owner of the email address wants to subscribe
|
||||
|
||||
That's where it would be over. Except for one technical detail that's also important:
|
||||
|
||||
## GET requests should not make changes
|
||||
|
||||
<Warning compact>This is more technical and harder to understand</Warning>
|
||||
|
||||
Another reason is that while we could make it so that clicking the link in your
|
||||
email would immeadiatly subcribe you, it would be in violation of internet standards.
|
||||
Specifically, the __HTTP__ protocol's __GET method__ definition which states that:
|
||||
|
||||
|
||||
<Note>
|
||||
<h5>GET requests should only retrieve data and should have no other effect.</h5>
|
||||
|
||||
[wikipedia.org/wiki/HTTP#HTTP/1.1_request_messages](https://en.wikipedia.org/wiki/HTTP#HTTP/1.1_request_messages)
|
||||
</Note>
|
||||
|
||||
A _GET request_ is what happens when you follow a link. Merely following a link
|
||||
should not make any changes (like subscribing you to a newsletter).
|
||||
|
||||
For example, when you receive an email, your email client
|
||||
may _preload_ the links in it in the background. So that they are quicker to
|
||||
load should you click on them.
|
||||
|
||||
Obviously, this preloading should not confirm your subscription. Which is why
|
||||
you need to click a button to confirm. Because that will trigger a __POST request__
|
||||
and those can make changes.
|
||||
|
||||
<Tip>
|
||||
|
||||
##### This does not apply to users subscribing through their account
|
||||
|
||||
None of this applies to users who subscribe to our newsletter by enabling the
|
||||
option in our account. In this case, we do not need to go through the email
|
||||
validation process, since we already did that when you signed up.
|
||||
|
||||
For users, subscribing (and unsubscribing) is instant (If you are curious,
|
||||
we use an idempotent __PUT request__ under the hood).
|
||||
</Tip>
|
||||
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
title: Why do I have to click again to confirm I want to subscribe after I already clicked the confirmation link you sent me?
|
||||
---
|
||||
|
||||
There are two aspects that cause signing up for our newsletter to require multiple clicks:
|
||||
|
||||
- [People should only be able to sign up themselves](#people-should-only-be-able-to-sign-up-themselves)
|
||||
- [GET requests should not make changes](#get-requests-should-not-make-changes)
|
||||
|
||||
## People should only be able to sign up themselves
|
||||
|
||||
This one is pretty easy to understand. One should not be able
|
||||
to subscribe somebody else's email address to the FreeSewing newsletter.
|
||||
|
||||
This is why, after indicating you want to sign up, we sent you a confirmation
|
||||
email to the email address you provided.
|
||||
If you receive this email, it confirms not only that the email address
|
||||
is working, but also that you have access to it.
|
||||
|
||||
In other words, only after you click the link in the confirmation email can we know for
|
||||
cerntain that:
|
||||
|
||||
- The email address is valid
|
||||
- The owner of the email address wants to subscribe
|
||||
|
||||
That's where it would be over. Except for one technical detail that's also important:
|
||||
|
||||
## GET requests should not make changes
|
||||
|
||||
<Warning compact>This is more technical and harder to understand</Warning>
|
||||
|
||||
Another reason is that while we could make it so that clicking the link in your
|
||||
email would immeadiatly subcribe you, it would be in violation of internet standards.
|
||||
Specifically, the __HTTP__ protocol's __GET method__ definition which states that:
|
||||
|
||||
|
||||
<Note>
|
||||
<h5>GET requests should only retrieve data and should have no other effect.</h5>
|
||||
|
||||
[wikipedia.org/wiki/HTTP#HTTP/1.1_request_messages](https://en.wikipedia.org/wiki/HTTP#HTTP/1.1_request_messages)
|
||||
</Note>
|
||||
|
||||
A _GET request_ is what happens when you follow a link. Merely following a link
|
||||
should not make any changes (like subscribing you to a newsletter).
|
||||
|
||||
For example, when you receive an email, your email client
|
||||
may _preload_ the links in it in the background. So that they are quicker to
|
||||
load should you click on them.
|
||||
|
||||
Obviously, this preloading should not confirm your subscription. Which is why
|
||||
you need to click a button to confirm. Because that will trigger a __POST request__
|
||||
and those can make changes.
|
||||
|
||||
<Tip>
|
||||
|
||||
##### This does not apply to users subscribing through their account
|
||||
|
||||
None of this applies to users who subscribe to our newsletter by enabling the
|
||||
option in our account. In this case, we do not need to go through the email
|
||||
validation process, since we already did that when you signed up.
|
||||
|
||||
For users, subscribing (and unsubscribing) is instant (If you are curious,
|
||||
we use an idempotent __PUT request__ under the hood).
|
||||
</Tip>
|
||||
|
||||
|
|
@ -24,9 +24,7 @@
|
|||
"rmdb": "node ./scripts/rmdb.mjs",
|
||||
"test": "npx mocha --require mocha-steps tests/index.mjs",
|
||||
"vbuild": "VERBOSE=1 node build.mjs",
|
||||
"prebuild": "node --conditions=internal --experimental-json-modules ./prebuild.mjs",
|
||||
"wbuild": "node build.mjs",
|
||||
"prewbuild": "node --conditions=internal --experimental-json-modules ./prebuild.mjs"
|
||||
"wbuild": "node build.mjs"
|
||||
},
|
||||
"peerDependencies": {},
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import { prebuildRunner } from '../shared/prebuild/runner.mjs'
|
||||
|
||||
/*
|
||||
* This handles the prebuild step for the FreeSewing backend
|
||||
* It runs via an NPM run script, so in a pure NodeJS context
|
||||
*
|
||||
* See `sites/org/prebuild.mjs` for an example with inline comments
|
||||
*/
|
||||
prebuildRunner({
|
||||
site: 'backend',
|
||||
prebuild: {
|
||||
i18n: true,
|
||||
},
|
||||
})
|
|
@ -30,10 +30,11 @@ model Confirmation {
|
|||
|
||||
model Subscriber {
|
||||
id String @id @default(uuid())
|
||||
active Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
data String
|
||||
ehash String @unique
|
||||
email String
|
||||
language String @default("en")
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@ const baseConfig = {
|
|||
env: process.env.NODE_ENV || 'development',
|
||||
// Maintainer contact
|
||||
maintainer: 'joost@freesewing.org',
|
||||
// Backend API base URL
|
||||
api: process.env.BACKEND_API_URL || 'https://backend3.freesewing.org',
|
||||
// Feature flags
|
||||
use: {
|
||||
github: envToBool(process.env.BACKEND_ENABLE_GITHUB),
|
||||
|
@ -44,12 +46,12 @@ const baseConfig = {
|
|||
},
|
||||
sanity: envToBool(process.env.BACKEND_ENABLE_SANITY),
|
||||
ses: envToBool(process.env.BACKEND_ENABLE_AWS_SES),
|
||||
stripe: envToBool(process.env.BACKEND_ENABLE_PAYMENTS),
|
||||
tests: {
|
||||
base: envToBool(process.env.BACKEND_ENABLE_TESTS),
|
||||
email: envToBool(process.env.BACKEND_ENABLE_TESTS_EMAIL),
|
||||
sanity: envToBool(process.env.BACKEND_ENABLE_TESTS_SANITY),
|
||||
},
|
||||
import: envToBool(process.env.BACKEND_ENABLE_IMPORT),
|
||||
},
|
||||
// Config
|
||||
api,
|
||||
|
@ -86,7 +88,7 @@ const baseConfig = {
|
|||
audience: process.env.BACKEND_JWT_ISSUER || 'freesewing.org',
|
||||
expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d',
|
||||
},
|
||||
languages: ['en', 'de', 'es', 'fr', 'nl'],
|
||||
languages: ['en', 'de', 'es', 'fr', 'nl', 'uk'],
|
||||
translations: ['de', 'es', 'fr', 'nl', 'uk'],
|
||||
measies: measurements,
|
||||
mfa: {
|
||||
|
@ -145,7 +147,7 @@ if (baseConfig.use.github)
|
|||
if (baseConfig.use.sanity)
|
||||
baseConfig.sanity = {
|
||||
project: process.env.SANITY_PROJECT,
|
||||
dataset: process.env.SANITY_DATASET || 'production',
|
||||
dataset: process.env.SANITY_DATASET || 'site-content',
|
||||
token: process.env.SANITY_TOKEN || 'fixmeSetSanityToken',
|
||||
version: process.env.SANITY_VERSION || 'v2022-10-31',
|
||||
api: `https://${process.env.SANITY_PROJECT || 'missing-project-id'}.api.sanity.io/${
|
||||
|
@ -153,14 +155,6 @@ if (baseConfig.use.sanity)
|
|||
}`,
|
||||
}
|
||||
|
||||
// Stripe config
|
||||
if (baseConfig.use.stripe)
|
||||
baseConfig.stripe = {
|
||||
keys: {
|
||||
createIntent: process.env.BACKEND_STRIPE_CREATE_INTENT_KEY || false,
|
||||
},
|
||||
}
|
||||
|
||||
// AWS SES config (for sending out emails)
|
||||
if (baseConfig.use.ses)
|
||||
baseConfig.aws = {
|
||||
|
@ -260,10 +254,6 @@ if (envToBool(process.env.BACKEND_ENABLE_OAUTH_GOOGLE)) {
|
|||
vars.BACKEND_OAUTH_GOOGLE_CLIENT_ID = 'required'
|
||||
vars.BACKEND_OAUTH_GOOGLE_CLIENT_SECRET = 'requiredSecret'
|
||||
}
|
||||
// Vars for Stripe integration
|
||||
if (envToBool(process.env.BACKEND_ENABLE_PAYMENTS)) {
|
||||
vars.BACKEND_STRIPE_CREATE_INTENT_KEY = 'requiredSecret'
|
||||
}
|
||||
|
||||
// Vars for (unit) tests
|
||||
if (envToBool(process.env.BACKEND_ENABLE_TESTS)) {
|
||||
|
@ -339,11 +329,6 @@ export function verifyConfig(silent = false) {
|
|||
config.jwt.secretOrKey.slice(0, 4) + '**redacted**' + config.jwt.secretOrKey.slice(-4),
|
||||
},
|
||||
}
|
||||
if (config.stripe)
|
||||
dump.stripe = {
|
||||
...config.stripe.keys,
|
||||
//token: config.sanity.token.slice(0, 4) + '**redacted**' + config.sanity.token.slice(-4),
|
||||
}
|
||||
if (config.sanity)
|
||||
dump.sanity = {
|
||||
...config.sanity,
|
||||
|
|
18
sites/backend/src/controllers/imports.mjs
Normal file
18
sites/backend/src/controllers/imports.mjs
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { ImportModel } from '../models/import.mjs'
|
||||
|
||||
export function ImportsController() {}
|
||||
|
||||
/*
|
||||
* Imports newsletters subscribers in v2 format
|
||||
*/
|
||||
ImportsController.prototype.subscribers = async (req, res, tools) => {
|
||||
/*
|
||||
* This is a special route that uses hard-coded credentials
|
||||
*/
|
||||
console.log(req.body)
|
||||
|
||||
const Import = new ImportModel(tools)
|
||||
//await Flow.sendTranslatorInvite(req)
|
||||
|
||||
return Import.sendResponse(res)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
export function PaymentsController() {}
|
||||
|
||||
/*
|
||||
* Create payment intent
|
||||
*
|
||||
* This is the endpoint that handles creation of a Stripe payment intent
|
||||
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||
*/
|
||||
PaymentsController.prototype.createIntent = async (req, res, tools) => {
|
||||
if (!req.body.amount) return this.setResponse(400, 'amountMissing')
|
||||
if (!req.body.currency) return this.setResponse(400, 'currencyMissing')
|
||||
|
||||
const body = new URLSearchParams()
|
||||
body.append('amount', req.body.amount)
|
||||
body.append('currency', req.body.currency)
|
||||
|
||||
// Call Stripe API with fetch to create the payment inent
|
||||
const result = await fetch('https://api.stripe.com/v1/payment_intents', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
authorization: `Bearer ${tools.config.stripe.keys.createIntent}`,
|
||||
},
|
||||
body,
|
||||
})
|
||||
// Convert response to JSON
|
||||
const json = await result.json()
|
||||
|
||||
// Return status code 201 (created) and clientSecret
|
||||
return res.status(201).send({
|
||||
result: 'success',
|
||||
clientSecret: json.client_secret,
|
||||
})
|
||||
}
|
68
sites/backend/src/controllers/subscribers.mjs
Normal file
68
sites/backend/src/controllers/subscribers.mjs
Normal file
|
@ -0,0 +1,68 @@
|
|||
//import fs from 'fs'
|
||||
//import path from 'path'
|
||||
import { SubscriberModel } from '../models/subscriber.mjs'
|
||||
|
||||
/*
|
||||
* Load pages for subscribe and unsubscribe confirmation
|
||||
* Some people might find this extra step annoying,
|
||||
* but GET requests should not make changes.
|
||||
*/
|
||||
//const index = fs.readFileSync(path.resolve('.', 'src', 'landing', 'index.html'))
|
||||
//app.get('/', async (req, res) => res.set('Content-Type', 'text/html').status(200).send(index))
|
||||
|
||||
export function SubscribersController() {}
|
||||
|
||||
/*
|
||||
* Subscribe to the newsletter (sends confirmation email)
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
SubscribersController.prototype.subscribe = async (req, res, tools) => {
|
||||
const Subscriber = new SubscriberModel(tools)
|
||||
await Subscriber.guardedCreate(req)
|
||||
|
||||
return Subscriber.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Subscribe confirmation
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
SubscribersController.prototype.subscribeConfirm = async (req, res, tools) => {
|
||||
const Subscriber = new SubscriberModel(tools)
|
||||
await Subscriber.subscribeConfirm(req)
|
||||
|
||||
return Subscriber.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Unsubscribe confirmation
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
SubscribersController.prototype.unsubscribeConfirm = async (req, res, tools) => {
|
||||
const Subscriber = new SubscriberModel(tools)
|
||||
await Subscriber.unsubscribeConfirm(req)
|
||||
|
||||
return Subscriber.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Confirm newsletter subscription
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
SubscribersController.prototype.confirm = async (req, res, tools) => {
|
||||
const Subscriber = new SubscriberModel(tools)
|
||||
await Subscriber.confirm(req)
|
||||
|
||||
return Subscriber.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Unsubscribe from the newsletter
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
SubscribersController.unsubscribe = async (req, res, tools) => {
|
||||
const Subscriber = new SubscriberModel(tools)
|
||||
await Subscriber.unsubscribe(req)
|
||||
|
||||
return Subscriber.sendResponse(res)
|
||||
}
|
18
sites/backend/src/html/catch-all.mjs
Normal file
18
sites/backend/src/html/catch-all.mjs
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { wrapper } from './shared.mjs'
|
||||
|
||||
export const html = wrapper({
|
||||
content: `
|
||||
<h1><span role="img">👋</span></h1>
|
||||
<h2>This is the FreeSewing backend</h2>
|
||||
<p>
|
||||
For info about FreeSewing, try <a href="https://freesewing.org/">freesewing.org</a> instead.
|
||||
</p>
|
||||
<p>
|
||||
For info about this backend, refer to <a href="https://freesewing.dev/reference/backend">the FreeSewing backend refefence documentation</a>.
|
||||
</p>
|
||||
<p>
|
||||
For questions, join us at
|
||||
<a href="https://discord.freesewing.org/">discord.freesewing.org</a>
|
||||
</p>
|
||||
`,
|
||||
})
|
16
sites/backend/src/html/shared.mjs
Normal file
16
sites/backend/src/html/shared.mjs
Normal file
|
@ -0,0 +1,16 @@
|
|||
export const wrapper = ({ title = 'FreeSewing', header, content, footer }) => `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
${header ? header : '<span></span>'}
|
||||
<div class="msg">${content}</div>
|
||||
<img src="/avatar.svg" />
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
|
@ -1,8 +1,6 @@
|
|||
// Dependencies
|
||||
import express from 'express'
|
||||
import chalk from 'chalk'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import passport from 'passport'
|
||||
// Routes
|
||||
|
@ -22,6 +20,8 @@ import { mailer } from './utils/email.mjs'
|
|||
// Swagger
|
||||
import swaggerUi from 'swagger-ui-express'
|
||||
import { openapi } from '../openapi/index.mjs'
|
||||
// Catch-all page
|
||||
import { html as catchAll } from './html/catch-all.mjs'
|
||||
|
||||
// Bootstrap
|
||||
const config = verifyConfig()
|
||||
|
@ -49,9 +49,7 @@ loadPassportMiddleware(passport, tools)
|
|||
// Load routes
|
||||
for (const type in routes) routes[type](tools)
|
||||
|
||||
// Catch-all route (Load index.html once instead of at every request)
|
||||
const index = fs.readFileSync(path.resolve('.', 'src', 'landing', 'index.html'))
|
||||
app.get('/', async (req, res) => res.set('Content-Type', 'text/html').status(200).send(index))
|
||||
app.get('/', async (req, res) => res.set('Content-Type', 'text/html').status(200).send(catchAll))
|
||||
|
||||
// Start listening for requests
|
||||
app.listen(config.port, (err) => {
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FreeSewing backend</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<span></span>
|
||||
<div class="msg">
|
||||
<h1><span role="img">👋</span></h1>
|
||||
<h2>This is the FreeSewing backend</h2>
|
||||
<p>
|
||||
For info about FreeSewing, try <a href="https://freesewing.org/">freesewing.org</a> instead.
|
||||
</p>
|
||||
<p>
|
||||
For info about this backend, refer to <a href="https://freesewing.dev/reference/backend">the FreeSewing backend refefence documentation</a>.
|
||||
</p>
|
||||
<p>
|
||||
For questions, join us at
|
||||
<a href="https://discord.freesewing.org/">discord.freesewing.org</a>
|
||||
</p>
|
||||
</div>
|
||||
<img src="/avatar.svg" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,52 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Your request is invalid</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Raleway:300,500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
color: #f8f9fa;
|
||||
line-height: 1.25;
|
||||
font-size: 24px;
|
||||
background: #212529;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Raleway', sans-serif;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 2rem;
|
||||
}
|
||||
img {
|
||||
width: 166px;
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: calc(50% - 83px);
|
||||
}
|
||||
div.msg {
|
||||
text-align: left;
|
||||
max-width: 36ch;
|
||||
margin: 6rem auto;
|
||||
}
|
||||
a,
|
||||
a:visited,
|
||||
a:active {
|
||||
color: #d0bfff !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="msg">
|
||||
<h1>What are you doing?</h1>
|
||||
<p>Try <a href="https://freesewing.org/">freesewing.org</a> instead.</p>
|
||||
</div>
|
||||
<img src="https://freesewing.org/avatar.svg" />
|
||||
</body>
|
||||
</html>
|
297
sites/backend/src/models/subscriber.mjs
Normal file
297
sites/backend/src/models/subscriber.mjs
Normal file
|
@ -0,0 +1,297 @@
|
|||
import { hash, randomString } from '../utils/crypto.mjs'
|
||||
import { setUserAvatar } from '../utils/sanity.mjs'
|
||||
import { log } from '../utils/log.mjs'
|
||||
import { clean, i18nUrl } from '../utils/index.mjs'
|
||||
|
||||
export function SubscriberModel(tools) {
|
||||
this.config = tools.config
|
||||
this.prisma = tools.prisma
|
||||
this.mailer = tools.email
|
||||
this.decrypt = tools.decrypt
|
||||
this.encrypt = tools.encrypt
|
||||
this.encryptedFields = ['email']
|
||||
this.clear = {} // For holding decrypted data
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
SubscriberModel.prototype.guardedCreate = async function ({ body }) {
|
||||
if (!body.email || typeof body.email !== 'string') return this.setResponse(400, 'emailMissing')
|
||||
if (!body.language || !this.config.languages.includes(body.language.toLowerCase()))
|
||||
return this.setResponse(400, 'languageMissing')
|
||||
|
||||
// Clean up email address and hash it
|
||||
const email = clean(body.email)
|
||||
const language = body.language.toLowerCase()
|
||||
const ehash = hash(email)
|
||||
|
||||
log.info(`New newsletter subscriber: ${email}`)
|
||||
|
||||
// Check whether this is a unit test
|
||||
const isTest = this.isTest(body)
|
||||
|
||||
// Check to see if this email address is already subscribed.
|
||||
let newSubscriber = false
|
||||
await this.read({ ehash })
|
||||
|
||||
if (!this.record) {
|
||||
// No record found. Create subscriber record.
|
||||
newSubscriber = true
|
||||
const data = await this.cloak({ ehash, email, language, active: false })
|
||||
try {
|
||||
this.record = await this.prisma.subscriber.create({ data })
|
||||
} catch (err) {
|
||||
log.warn(err, 'Could not create subscriber record')
|
||||
return this.setResponse(500, 'createSubscriberFailed')
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the various URLs
|
||||
const actionUrl = i18nUrl(
|
||||
`/newsletter/${this.record.active ? 'un' : ''}subscribe/${this.record.id}/${ehash}`
|
||||
)
|
||||
|
||||
// Send out confirmation email unless it's a test and we don't want to send test emails
|
||||
if (!isTest || this.config.use.tests.email) {
|
||||
const template = newSubscriber ? 'nlsub' : this.record.active ? 'nlsubact' : 'nlsubinact'
|
||||
|
||||
await this.mailer.send({
|
||||
template,
|
||||
language,
|
||||
to: email,
|
||||
replacements: {
|
||||
actionUrl,
|
||||
whyUrl: i18nUrl(body.language, `/docs/faq/email/why-${template}`),
|
||||
supportUrl: i18nUrl(body.language, `/patrons/join`),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const returnData = { language, email }
|
||||
if (isTest) {
|
||||
returnData.id = this.record.id
|
||||
returnData.ehash = ehash
|
||||
}
|
||||
|
||||
return this.setResponse(200, 'success', { data: returnData })
|
||||
}
|
||||
|
||||
SubscriberModel.prototype.subscribeConfirm = async function ({ body }) {
|
||||
const { id, ehash } = body
|
||||
if (!id) return this.setResponse(400, 'idMissing')
|
||||
if (!ehash) return this.setResponse(400, 'ehashMissing')
|
||||
|
||||
// Find subscription
|
||||
await this.read({ ehash })
|
||||
|
||||
if (!this.record) {
|
||||
// Subscriber not found
|
||||
return this.setResponse(404, 'subscriberNotFound')
|
||||
}
|
||||
|
||||
if (this.record.status !== true) {
|
||||
// Update username
|
||||
try {
|
||||
await this.unguardedUpdate({ active: true })
|
||||
} catch (err) {
|
||||
log.warn(err, 'Could not update active state after subscribe confirmation')
|
||||
return this.setResponse(500, 'subscriberActivationFailed')
|
||||
}
|
||||
}
|
||||
|
||||
return this.setResponse(200, 'success')
|
||||
}
|
||||
|
||||
SubscriberModel.prototype.unsubscribeConfirm = async function ({ body }) {
|
||||
const { id, ehash } = body
|
||||
if (!id) return this.setResponse(400, 'idMissing')
|
||||
if (!ehash) return this.setResponse(400, 'ehashMissing')
|
||||
|
||||
// Find subscription
|
||||
await this.read({ ehash })
|
||||
|
||||
if (this.record) {
|
||||
// Remove record
|
||||
try {
|
||||
await this.unguardedDelete()
|
||||
} catch (err) {
|
||||
log.warn(err, 'Could not remove subscriber')
|
||||
return this.setResponse(500, 'subscriberRemovalFailed')
|
||||
}
|
||||
}
|
||||
|
||||
return this.setResponse(200, 'success')
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates the subscriber data
|
||||
* Used when we create the data ourselves so we know it's safe
|
||||
*/
|
||||
SubscriberModel.prototype.unguardedUpdate = async function (data) {
|
||||
try {
|
||||
this.record = await this.prisma.subscriber.update({
|
||||
where: { id: this.record.id },
|
||||
data,
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn(err, 'Could not update subscriber record')
|
||||
process.exit()
|
||||
return this.setResponse(500, 'updateSubscriberFailed')
|
||||
}
|
||||
await this.reveal()
|
||||
|
||||
return this.setResponse(200)
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes the subscriber record
|
||||
* Used when we call for removal ourselves so we know it's safe
|
||||
*/
|
||||
SubscriberModel.prototype.unguardedDelete = async function () {
|
||||
await this.prisma.subscriber.delete({ where: { id: this.record.id } })
|
||||
this.record = null
|
||||
this.clear = null
|
||||
|
||||
return this.subscriberExists()
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads a subscriber from the database based on the where clause you pass it
|
||||
*
|
||||
* Stores result in this.record
|
||||
*/
|
||||
SubscriberModel.prototype.read = async function (where) {
|
||||
try {
|
||||
this.record = await this.prisma.subscriber.findUnique({ where })
|
||||
} catch (err) {
|
||||
log.warn({ err, where }, 'Could not read subscriber')
|
||||
}
|
||||
|
||||
this.reveal()
|
||||
|
||||
return this.subscriberExists()
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks this.record and sets a boolean to indicate whether
|
||||
* the subscription exists or not
|
||||
*
|
||||
* Stores result in this.exists
|
||||
*/
|
||||
SubscriberModel.prototype.subscriberExists = function () {
|
||||
this.exists = this.record ? true : false
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads a measurements set from the database based on the where clause you pass it
|
||||
* In addition prepares it for returning the set data
|
||||
*
|
||||
* Stores result in this.record
|
||||
*/
|
||||
SubscriberModel.prototype.guardedRead = async function ({ params, user }) {
|
||||
if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
await this.read({ id: parseInt(params.id) })
|
||||
if (!this.record) return this.setResponse(404)
|
||||
if (this.record.userId !== user.uid && !this.rbac.bughunter(user)) {
|
||||
return this.setResponse(403, 'insufficientAccessLevel')
|
||||
}
|
||||
|
||||
return this.setResponse(200, false, {
|
||||
result: 'success',
|
||||
set: this.asSet(),
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to decrypt at-rest data
|
||||
*/
|
||||
SubscriberModel.prototype.reveal = async function () {
|
||||
this.clear = {}
|
||||
if (this.record) {
|
||||
for (const field of this.encryptedFields) {
|
||||
try {
|
||||
this.clear[field] = this.decrypt(this.record[field])
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to encrypt at-rest data
|
||||
*/
|
||||
SubscriberModel.prototype.cloak = function (data) {
|
||||
for (const field of this.encryptedFields) {
|
||||
if (typeof data[field] !== 'undefined') {
|
||||
data[field] = this.encrypt(data[field])
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes the subscriber - No questions asked
|
||||
*/
|
||||
SubscriberModel.prototype.unguardedDelete = async function () {
|
||||
await this.prisma.subscriber.delete({ where: { id: this.record.id } })
|
||||
this.record = null
|
||||
this.clear = null
|
||||
|
||||
return this.subscriberExists()
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to set the response code, result, and body
|
||||
*
|
||||
* Will be used by this.sendResponse()
|
||||
*/
|
||||
SubscriberModel.prototype.setResponse = function (
|
||||
status = 200,
|
||||
error = false,
|
||||
data = {},
|
||||
rawData = false
|
||||
) {
|
||||
this.response = {
|
||||
status,
|
||||
body: rawData
|
||||
? data
|
||||
: {
|
||||
result: 'success',
|
||||
...data,
|
||||
},
|
||||
}
|
||||
if (status > 201) {
|
||||
this.response.body.error = error
|
||||
this.response.body.result = 'error'
|
||||
this.error = true
|
||||
} else this.error = false
|
||||
|
||||
return this.subscriberExists()
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to send response (as JSON)
|
||||
*/
|
||||
SubscriberModel.prototype.sendResponse = async function (res) {
|
||||
return res.status(this.response.status).send(this.response.body)
|
||||
}
|
||||
|
||||
/*
|
||||
* Update method to determine whether this request is part of a (unit) test
|
||||
*/
|
||||
SubscriberModel.prototype.isTest = function (body) {
|
||||
// Disalowing tests in prodution is hard-coded to protect people from themselves
|
||||
if (this.config.env === 'production' && !this.config.tests.production) return false
|
||||
if (!body.test) return false
|
||||
if (body.email && !body.email.split('@').pop() === this.config.tests.domain) return false
|
||||
|
||||
return true
|
||||
}
|
14
sites/backend/src/routes/import.mjs
Normal file
14
sites/backend/src/routes/import.mjs
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { ImportController } from '../controllers/import.mjs'
|
||||
|
||||
const Import = new ImportController()
|
||||
|
||||
export function importsRoutes(tools) {
|
||||
const { app } = tools
|
||||
|
||||
/*
|
||||
* All these routes use hard-coded credentials because they should never be used
|
||||
* outside the v2-v3 migration which is handled by joost
|
||||
*/
|
||||
// Import newsletter subscriptions
|
||||
app.post('/import/subscribers', (req, res) => Import.subscribers(req, res, tools))
|
||||
}
|
|
@ -5,8 +5,8 @@ import { patternsRoutes } from './patterns.mjs'
|
|||
import { confirmationsRoutes } from './confirmations.mjs'
|
||||
import { curatedSetsRoutes } from './curated-sets.mjs'
|
||||
import { issuesRoutes } from './issues.mjs'
|
||||
import { subscribersRoutes } from './subscribers.mjs'
|
||||
import { flowsRoutes } from './flows.mjs'
|
||||
import { paymentsRoutes } from './payments.mjs'
|
||||
|
||||
export const routes = {
|
||||
apikeysRoutes,
|
||||
|
@ -16,6 +16,6 @@ export const routes = {
|
|||
confirmationsRoutes,
|
||||
curatedSetsRoutes,
|
||||
issuesRoutes,
|
||||
subscribersRoutes,
|
||||
flowsRoutes,
|
||||
paymentsRoutes,
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import { PaymentsController } from '../controllers/payments.mjs'
|
||||
|
||||
const Payments = new PaymentsController()
|
||||
|
||||
export function paymentsRoutes(tools) {
|
||||
const { app } = tools
|
||||
|
||||
/*
|
||||
* Only add these routes if payments are enabled in the config
|
||||
*/
|
||||
if (tools.config.use.stripe) {
|
||||
// Create payment intent
|
||||
app.post('/payments/intent', (req, res) => Payments.createIntent(req, res, tools))
|
||||
}
|
||||
}
|
20
sites/backend/src/routes/subscribers.mjs
Normal file
20
sites/backend/src/routes/subscribers.mjs
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { SubscribersController } from '../controllers/subscribers.mjs'
|
||||
|
||||
const Subscriber = new SubscribersController()
|
||||
|
||||
export function subscribersRoutes(tools) {
|
||||
const { app } = tools
|
||||
|
||||
/*
|
||||
* None of these require authentication
|
||||
*/
|
||||
|
||||
// Subscribe to the newsletter
|
||||
app.post('/subscriber', (req, res) => Subscriber.subscribe(req, res, tools))
|
||||
|
||||
// Confirm subscription to the newsletter
|
||||
app.put('/subscriber', (req, res) => Subscriber.subscribeConfirm(req, res, tools))
|
||||
|
||||
// Unsubscribe from newsletter
|
||||
app.delete('/subscriber', (req, res) => Subscriber.unsubscribeConfirm(req, res, tools))
|
||||
}
|
10
sites/backend/src/templates/email/de.json
Normal file
10
sites/backend/src/templates/email/de.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"greeting": "Ganz liebe Grüße",
|
||||
"ps-pre-link": "FreeSewing ist kostenlos (duh), aber bitte",
|
||||
"ps-link": "werde ein/e Förderer/-in",
|
||||
"ps-post-link": "wenn du es dir leisten kannst.",
|
||||
"text-ps": "FreeSewing ist kostenlos, aber wenn du es dir leisten kannst, werde bitte Förderer/-in.",
|
||||
"notMarketing": "Dies ist keine Marketing-E-Mail, sondern eine Transaktions-E-Mail über dein FreeSewing-Konto.",
|
||||
"seeWhy": "Weitere Informationen findest du hier:",
|
||||
"whyDidIGetThis": "Warum habe ich diese E-Mail bekommen?"
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
#Shared
|
||||
greeting: Ganz liebe Grüße
|
||||
ps-pre-link: FreeSewing ist kostenlos (duh), aber bitte
|
||||
ps-link: werde ein/e Förderer/-in
|
||||
ps-post-link: wenn du es dir leisten kannst.
|
||||
text-ps: 'FreeSewing ist kostenlos, aber wenn du es dir leisten kannst, werde bitte Förderer/-in.'
|
||||
notMarketing: Dies ist keine Marketing-E-Mail, sondern eine Transaktions-E-Mail über dein FreeSewing-Konto.
|
||||
seeWhy: 'Weitere Informationen findest du hier:'
|
||||
whyDidIGetThis: Warum habe ich diese E-Mail bekommen?
|
8
sites/backend/src/templates/email/emailchange/de.json
Normal file
8
sites/backend/src/templates/email/emailchange/de.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Bestätige deine neue E-Mail-Adresse",
|
||||
"heading": "Funktioniert diese neue E-Mail-Adresse?",
|
||||
"lead": "Um deine neue E-Mail-Adresse zu bestätigen, klicke auf das große schwarze Rechteck unten:",
|
||||
"text-lead": "Um deine neue E-Mail-Adresse zu bestätigen, klicke auf den Link unten:",
|
||||
"button": "Bestätige die E-Mail-Änderung",
|
||||
"closing": "Mehr muss nicht getan werden."
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
subject: "[FreeSewing] Bestätige deine neue E-Mail-Adresse"
|
||||
heading: Funktioniert diese neue E-Mail-Adresse?
|
||||
lead: 'Um deine neue E-Mail-Adresse zu bestätigen, klicke auf das große schwarze Rechteck unten:'
|
||||
text-lead: 'Um deine neue E-Mail-Adresse zu bestätigen, klicke auf den Link unten:'
|
||||
button: Bestätige die E-Mail-Änderung
|
||||
closing: Mehr muss nicht getan werden.
|
8
sites/backend/src/templates/email/emailchange/en.json
Normal file
8
sites/backend/src/templates/email/emailchange/en.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your new E-mail address",
|
||||
"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."
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
subject: "[FreeSewing] Confirm your new E-mail address"
|
||||
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.
|
8
sites/backend/src/templates/email/emailchange/es.json
Normal file
8
sites/backend/src/templates/email/emailchange/es.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirma tu nueva dirección de correo electrónico",
|
||||
"heading": "¿Funciona esta nueva dirección de correo electrónico?",
|
||||
"lead": "Para confirmar tu nueva dirección de correo electrónico, haz clic en el rectángulo negro grande de abajo:",
|
||||
"text-lead": "Para confirmar tu nueva dirección de correo electrónico, haz clic en el siguiente enlace:",
|
||||
"button": "Confirmar cambio de e-mail",
|
||||
"closing": "Eso es todo lo que hace falta."
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
subject: "[FreeSewing] Confirma tu nueva dirección de correo electrónico"
|
||||
heading: '¿Funciona esta nueva dirección de correo electrónico?'
|
||||
lead: 'Para confirmar tu nueva dirección de correo electrónico, haz clic en el rectángulo negro grande de abajo:'
|
||||
text-lead: 'Para confirmar tu nueva dirección de correo electrónico, haz clic en el siguiente enlace:'
|
||||
button: Confirmar cambio de e-mail
|
||||
closing: Eso es todo lo que hace falta.
|
8
sites/backend/src/templates/email/emailchange/fr.json
Normal file
8
sites/backend/src/templates/email/emailchange/fr.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirme ta nouvelle adresse e-mail",
|
||||
"heading": "Cette nouvelle adresse électronique fonctionne-t-elle ?",
|
||||
"lead": "Pour confirmer ta nouvelle adresse e-mail, clique sur le grand rectangle noir ci-dessous :",
|
||||
"text-lead": "Pour confirmer ta nouvelle adresse e-mail, clique sur le lien ci-dessous :",
|
||||
"button": "Confirmer le changement d'e-mail",
|
||||
"closing": "C'est tout ce qu'il faut."
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
subject: "[FreeSewing] Confirme ta nouvelle adresse e-mail"
|
||||
heading: Cette nouvelle adresse électronique fonctionne-t-elle ?
|
||||
lead: 'Pour confirmer ta nouvelle adresse e-mail, clique sur le grand rectangle noir ci-dessous :'
|
||||
text-lead: 'Pour confirmer ta nouvelle adresse e-mail, clique sur le lien ci-dessous :'
|
||||
button: Confirmer le changement d'e-mail
|
||||
closing: C'est tout ce qu'il faut.
|
|
@ -1,10 +1,11 @@
|
|||
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' }
|
||||
import en from './en.json' assert { type: 'json' }
|
||||
import de from './de.json' assert { type: 'json' }
|
||||
import es from './es.json' assert { type: 'json' }
|
||||
import fr from './fr.json' assert { type: 'json' }
|
||||
import nl from './nl.json' assert { type: 'json' }
|
||||
import uk from './uk.json' assert { type: 'json' }
|
||||
|
||||
export const emailchange = {
|
||||
html: wrap.html(`
|
||||
|
@ -16,4 +17,4 @@ export const emailchange = {
|
|||
text: wrap.text(`${headingRow.text}${lead1Row.text}${buttonRow.text}${closingRow.text}`),
|
||||
}
|
||||
|
||||
export const translations = { en, de, es, fr, nl }
|
||||
export const translations = { en, de, es, fr, nl, uk }
|
||||
|
|
8
sites/backend/src/templates/email/emailchange/nl.json
Normal file
8
sites/backend/src/templates/email/emailchange/nl.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Bevestig je nieuwe e-mailadres",
|
||||
"heading": "Werkt dit nieuwe e-mailadres?",
|
||||
"lead": "Klik op de grote zwarte rechthoek hieronder om je nieuwe e-mailadres te bevestigen:",
|
||||
"text-lead": "Klik op de link hieronder om je nieuwe e-mailadres te bevestigen:",
|
||||
"button": "E-mailwijziging bevestigen",
|
||||
"closing": "Langer hoeft dat niet te duren."
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
subject: "[FreeSewing] Bevestig je nieuwe e-mailadres"
|
||||
heading: Werkt dit nieuwe e-mailadres?
|
||||
lead: 'Klik op de grote zwarte rechthoek hieronder om je nieuwe e-mailadres te bevestigen:'
|
||||
text-lead: 'Klik op de link hieronder om je nieuwe e-mailadres te bevestigen:'
|
||||
button: E-mailwijziging bevestigen
|
||||
closing: Langer hoeft dat niet te duren.
|
8
sites/backend/src/templates/email/emailchange/uk.json
Normal file
8
sites/backend/src/templates/email/emailchange/uk.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Підтвердіть свою нову електронну адресу",
|
||||
"heading": "Чи працює ця нова електронна адреса?",
|
||||
"lead": "Щоб підтвердити свою нову електронну адресу, натисніть на великий чорний прямокутник нижче:",
|
||||
"text-lead": "Щоб підтвердити свою нову електронну адресу, перейдіть за посиланням нижче:",
|
||||
"button": "Підтвердити зміну електронної пошти",
|
||||
"closing": "Це все, що потрібно."
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
subject: "[FreeSewing] Підтвердіть свою нову електронну адресу"
|
||||
heading: Чи працює ця нова електронна адреса?
|
||||
lead: 'Щоб підтвердити свою нову електронну адресу, натисніть на великий чорний прямокутник нижче:'
|
||||
text-lead: 'Щоб підтвердити свою нову електронну адресу, перейдіть за посиланням нижче:'
|
||||
button: Підтвердити зміну електронної пошти
|
||||
closing: Це все, що потрібно.
|
10
sites/backend/src/templates/email/en.json
Normal file
10
sites/backend/src/templates/email/en.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"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?"
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
# 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?
|
10
sites/backend/src/templates/email/es.json
Normal file
10
sites/backend/src/templates/email/es.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"greeting": "con amor",
|
||||
"ps-pre-link": "FreeSewing es gratis (duh), pero por favor",
|
||||
"ps-link": "conviértete en un mecenas",
|
||||
"ps-post-link": "si te lo puedes permitir.",
|
||||
"text-ps": "FreeSewing es gratis (duh), pero hazte mecenas si puedes permitírtelo.",
|
||||
"notMarketing": "No se trata de marketing, sino de un correo electrónico transaccional sobre tu cuenta de FreeSewing.",
|
||||
"seeWhy": "Para más información, consulta:",
|
||||
"whyDidIGetThis": "¿Por qué he recibido este correo electrónico?"
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
#Shared
|
||||
greeting: con amor
|
||||
ps-pre-link: FreeSewing es gratis (duh), pero por favor
|
||||
ps-link: conviértete en un mecenas
|
||||
ps-post-link: si te lo puedes permitir.
|
||||
text-ps: 'FreeSewing es gratis (duh), pero hazte mecenas si puedes permitírtelo.'
|
||||
notMarketing: No se trata de marketing, sino de un correo electrónico transaccional sobre tu cuenta de FreeSewing.
|
||||
seeWhy: 'Para más información, consulta:'
|
||||
whyDidIGetThis: '¿Por qué he recibido este correo electrónico?'
|
10
sites/backend/src/templates/email/fr.json
Normal file
10
sites/backend/src/templates/email/fr.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"greeting": "bise",
|
||||
"ps-pre-link": "FreeSewing est gratuit (duh), mais s'il te plaît...",
|
||||
"ps-link": "devenir mécène",
|
||||
"ps-post-link": "si tu peux te le permettre.",
|
||||
"text-ps": "FreeSewing est gratuit (duh), mais deviens un mécène si tu peux te le permettre.",
|
||||
"notMarketing": "Il ne s'agit pas de marketing, mais d'un email transactionnel concernant ton compte FreeSewing.",
|
||||
"seeWhy": "Pour plus d'infos, voir :",
|
||||
"whyDidIGetThis": "Pourquoi ai-je reçu cet e-mail ?"
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
#Shared
|
||||
greeting: bise
|
||||
ps-pre-link: FreeSewing est gratuit (duh), mais s'il te plaît...
|
||||
ps-link: devenir mécène
|
||||
ps-post-link: si tu peux te le permettre.
|
||||
text-ps: 'FreeSewing est gratuit (duh), mais deviens un mécène si tu peux te le permettre.'
|
||||
notMarketing: Il ne s'agit pas de marketing, mais d'un email transactionnel concernant ton compte FreeSewing.
|
||||
seeWhy: 'Pour plus d''infos, voir :'
|
||||
whyDidIGetThis: Pourquoi ai-je reçu cet e-mail ?
|
7
sites/backend/src/templates/email/goodbye/de.json
Normal file
7
sites/backend/src/templates/email/goodbye/de.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Abschied nehmen",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] Abschied nehmen'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
7
sites/backend/src/templates/email/goodbye/en.json
Normal file
7
sites/backend/src/templates/email/goodbye/en.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Farewell",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] Farewell'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
7
sites/backend/src/templates/email/goodbye/es.json
Normal file
7
sites/backend/src/templates/email/goodbye/es.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Adiós",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] Adiós'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
7
sites/backend/src/templates/email/goodbye/fr.json
Normal file
7
sites/backend/src/templates/email/goodbye/fr.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Adieu",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] Adieu'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -1,10 +1,11 @@
|
|||
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' }
|
||||
import en from './en.json' assert { type: 'json' }
|
||||
import de from './de.json' assert { type: 'json' }
|
||||
import es from './es.json' assert { type: 'json' }
|
||||
import fr from './fr.json' assert { type: 'json' }
|
||||
import nl from './nl.json' assert { type: 'json' }
|
||||
import uk from './uk.json' assert { type: 'json' }
|
||||
|
||||
export const goodbye = {
|
||||
html: wrap.html(`
|
||||
|
@ -35,4 +36,4 @@ joost
|
|||
PS: {{ps}}`),
|
||||
}
|
||||
|
||||
export const translations = { en, de, es, fr, nl }
|
||||
export const translations = { en, de, es, fr, nl, uk }
|
||||
|
|
7
sites/backend/src/templates/email/goodbye/nl.json
Normal file
7
sites/backend/src/templates/email/goodbye/nl.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Vaarwel",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] Vaarwel'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
7
sites/backend/src/templates/email/goodbye/uk.json
Normal file
7
sites/backend/src/templates/email/goodbye/uk.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Прощавай.",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] Прощавай.'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -1,19 +1,22 @@
|
|||
import { emailchange, translations as emailchangeTranslations } from './emailchange/index.mjs'
|
||||
import { goodbye, translations as goodbyeTranslations } from './goodbye/index.mjs'
|
||||
import { signinlink, translations as signinlinkTranslations } from './signinlink/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'
|
||||
import { transinvite, translations as transinviteTranslations } from './transinvite/index.mjs'
|
||||
import { langsuggest } from './langsuggest/index.mjs'
|
||||
import { nlsub, translations as nlsubTranslations } from './nlsub/index.mjs'
|
||||
import { nlsubact, translations as nlsubactTranslations } from './nlsubact/index.mjs'
|
||||
import { nlsubinact, translations as nlsubinactTranslations } from './nlsubinact/index.mjs'
|
||||
// Shared translations
|
||||
import en from '../../../public/locales/en/email.json' assert { type: 'json' }
|
||||
import de from '../../../public/locales/de/email.json' assert { type: 'json' }
|
||||
import es from '../../../public/locales/es/email.json' assert { type: 'json' }
|
||||
import fr from '../../../public/locales/fr/email.json' assert { type: 'json' }
|
||||
import nl from '../../../public/locales/nl/email.json' assert { type: 'json' }
|
||||
import en from './en.json' assert { type: 'json' }
|
||||
import de from './de.json' assert { type: 'json' }
|
||||
import es from './es.json' assert { type: 'json' }
|
||||
import fr from './fr.json' assert { type: 'json' }
|
||||
import nl from './nl.json' assert { type: 'json' }
|
||||
import uk from './uk.json' assert { type: 'json' }
|
||||
|
||||
/*
|
||||
* Everything is kept lowercase here because these key names are used in URLS
|
||||
|
@ -22,13 +25,15 @@ export const templates = {
|
|||
emailchange,
|
||||
goodbye,
|
||||
signinlink,
|
||||
newslettersub,
|
||||
passwordreset,
|
||||
signup,
|
||||
'signup-aea': signupaea,
|
||||
'signup-aed': signupaed,
|
||||
transinvite,
|
||||
langsuggest,
|
||||
nlsub,
|
||||
nlsubact,
|
||||
nlsubinact,
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -47,12 +52,14 @@ export const translations = {
|
|||
emailchange: emailchangeTranslations,
|
||||
goodbye: goodbyeTranslations,
|
||||
signinlink: signinlinkTranslations,
|
||||
newslettersub: newslettersubTranslations,
|
||||
passwordreset: passwordresetTranslations,
|
||||
signup: signupTranslations,
|
||||
'signup-aea': signupaeaTranslations,
|
||||
'signup-aed': signupaedTranslations,
|
||||
transinvite: transinviteTranslations,
|
||||
langsuggest: noTranslations,
|
||||
shared: { en, de, es, fr, nl },
|
||||
nlsub: nlsubTranslations,
|
||||
nlsubact: nlsubactTranslations,
|
||||
nlsubinact: nlsubinactTranslations,
|
||||
shared: { en, de, es, fr, nl, uk },
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] newsletter sub fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] newsletter sub fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] boletín sub fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] newsletter sub fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -1,24 +0,0 @@
|
|||
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 }
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] nieuwsbrief sub fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] інформаційний бюлетень sub fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
10
sites/backend/src/templates/email/nl.json
Normal file
10
sites/backend/src/templates/email/nl.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"greeting": "liefs",
|
||||
"ps-pre-link": "FreeSewing is gratis (duh), maar alsjeblieft",
|
||||
"ps-link": "word mecenas",
|
||||
"ps-post-link": "als je het je kunt veroorloven.",
|
||||
"text-ps": "FreeSewing is gratis (duh), maar word alsjeblieft beschermer als je het je kunt veroorloven.",
|
||||
"notMarketing": "Dit is geen marketing, maar een transactie-e-mail over je FreeSewing account.",
|
||||
"seeWhy": "Zie voor meer informatie:",
|
||||
"whyDidIGetThis": "Waarom kreeg ik deze e-mail?"
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
#Shared
|
||||
greeting: liefs
|
||||
ps-pre-link: FreeSewing is gratis (duh), maar alsjeblieft
|
||||
ps-link: word mecenas
|
||||
ps-post-link: als je het je kunt veroorloven.
|
||||
text-ps: 'FreeSewing is gratis (duh), maar word alsjeblieft beschermer als je het je kunt veroorloven.'
|
||||
notMarketing: Dit is geen marketing, maar een transactie-e-mail over je FreeSewing account.
|
||||
seeWhy: 'Zie voor meer informatie:'
|
||||
whyDidIGetThis: Waarom kreeg ik deze e-mail?
|
9
sites/backend/src/templates/email/nlsub/de.json
Normal file
9
sites/backend/src/templates/email/nlsub/de.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now.",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsub/en.json
Normal file
9
sites/backend/src/templates/email/nlsub/en.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now.",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsub/es.json
Normal file
9
sites/backend/src/templates/email/nlsub/es.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now.",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsub/fr.json
Normal file
9
sites/backend/src/templates/email/nlsub/fr.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now.",
|
||||
"greeting": "love,"
|
||||
}
|
25
sites/backend/src/templates/email/nlsub/index.mjs
Normal file
25
sites/backend/src/templates/email/nlsub/index.mjs
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { buttonRow, newsletterClosingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs'
|
||||
// Translations
|
||||
import en from './en.json' assert { type: 'json' }
|
||||
import de from './de.json' assert { type: 'json' }
|
||||
import es from './es.json' assert { type: 'json' }
|
||||
import fr from './fr.json' assert { type: 'json' }
|
||||
import nl from './nl.json' assert { type: 'json' }
|
||||
import uk from './uk.json' assert { type: 'json' }
|
||||
|
||||
export const nlsub = {
|
||||
html: wrap.html(`
|
||||
${headingRow.html}
|
||||
${lead1Row.html}
|
||||
${buttonRow.html}
|
||||
${newsletterClosingRow.html}
|
||||
`),
|
||||
text: wrap.text(`
|
||||
${headingRow.text}
|
||||
${lead1Row.text}
|
||||
${buttonRow.text}
|
||||
${newsletterClosingRow.text}
|
||||
`),
|
||||
}
|
||||
|
||||
export const translations = { en, de, es, fr, nl, uk }
|
9
sites/backend/src/templates/email/nlsub/nl.json
Normal file
9
sites/backend/src/templates/email/nlsub/nl.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now.",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsub/uk.json
Normal file
9
sites/backend/src/templates/email/nlsub/uk.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now.",
|
||||
"greeting": "love,"
|
||||
}
|
6
sites/backend/src/templates/email/nlsubact/de.json
Normal file
6
sites/backend/src/templates/email/nlsubact/de.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"subject": "[FreeSewing] You are already subscribed to our newsletter",
|
||||
"heading": "You are already subscribed",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. But you are already subscribed, so there is nothing to do here.",
|
||||
"greeting": "love,"
|
||||
}
|
6
sites/backend/src/templates/email/nlsubact/en.json
Normal file
6
sites/backend/src/templates/email/nlsubact/en.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"subject": "[FreeSewing] You are already subscribed to our newsletter",
|
||||
"heading": "You are already subscribed",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. But you are already subscribed, so there is nothing to do here.",
|
||||
"greeting": "love,"
|
||||
}
|
6
sites/backend/src/templates/email/nlsubact/es.json
Normal file
6
sites/backend/src/templates/email/nlsubact/es.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"subject": "[FreeSewing] You are already subscribed to our newsletter",
|
||||
"heading": "You are already subscribed",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. But you are already subscribed, so there is nothing to do here.",
|
||||
"greeting": "love,"
|
||||
}
|
6
sites/backend/src/templates/email/nlsubact/fr.json
Normal file
6
sites/backend/src/templates/email/nlsubact/fr.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"subject": "[FreeSewing] You are already subscribed to our newsletter",
|
||||
"heading": "You are already subscribed",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. But you are already subscribed, so there is nothing to do here.",
|
||||
"greeting": "love,"
|
||||
}
|
23
sites/backend/src/templates/email/nlsubact/index.mjs
Normal file
23
sites/backend/src/templates/email/nlsubact/index.mjs
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs'
|
||||
// Translations
|
||||
import en from './en.json' assert { type: 'json' }
|
||||
import de from './de.json' assert { type: 'json' }
|
||||
import es from './es.json' assert { type: 'json' }
|
||||
import fr from './fr.json' assert { type: 'json' }
|
||||
import nl from './nl.json' assert { type: 'json' }
|
||||
import uk from './uk.json' assert { type: 'json' }
|
||||
|
||||
export const nlsubact = {
|
||||
html: wrap.html(`
|
||||
${headingRow.html}
|
||||
${lead1Row.html}
|
||||
${closingRow.html}
|
||||
`),
|
||||
text: wrap.text(`
|
||||
${headingRow.text}
|
||||
${lead1Row.text}
|
||||
${closingRow.text}
|
||||
`),
|
||||
}
|
||||
|
||||
export const translations = { en, de, es, fr, nl, uk }
|
6
sites/backend/src/templates/email/nlsubact/nl.json
Normal file
6
sites/backend/src/templates/email/nlsubact/nl.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"subject": "[FreeSewing] You are already subscribed to our newsletter",
|
||||
"heading": "You are already subscribed",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. But you are already subscribed, so there is nothing to do here.",
|
||||
"greeting": "love,"
|
||||
}
|
6
sites/backend/src/templates/email/nlsubact/uk.json
Normal file
6
sites/backend/src/templates/email/nlsubact/uk.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"subject": "[FreeSewing] You are already subscribed to our newsletter",
|
||||
"heading": "You are already subscribed",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. But you are already subscribed, so there is nothing to do here.",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsubinact/de.json
Normal file
9
sites/backend/src/templates/email/nlsubinact/de.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. (note that we already had a pending subscription for this address).",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsubinact/en.json
Normal file
9
sites/backend/src/templates/email/nlsubinact/en.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. (note that we already had a pending subscription for this address).",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsubinact/es.json
Normal file
9
sites/backend/src/templates/email/nlsubinact/es.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. (note that we already had a pending subscription for this address).",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsubinact/fr.json
Normal file
9
sites/backend/src/templates/email/nlsubinact/fr.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. (note that we already had a pending subscription for this address).",
|
||||
"greeting": "love,"
|
||||
}
|
25
sites/backend/src/templates/email/nlsubinact/index.mjs
Normal file
25
sites/backend/src/templates/email/nlsubinact/index.mjs
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { buttonRow, newsletterClosingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs'
|
||||
// Translations
|
||||
import en from './en.json' assert { type: 'json' }
|
||||
import de from './de.json' assert { type: 'json' }
|
||||
import es from './es.json' assert { type: 'json' }
|
||||
import fr from './fr.json' assert { type: 'json' }
|
||||
import nl from './nl.json' assert { type: 'json' }
|
||||
import uk from './uk.json' assert { type: 'json' }
|
||||
|
||||
export const nlsubinact = {
|
||||
html: wrap.html(`
|
||||
${headingRow.html}
|
||||
${lead1Row.html}
|
||||
${buttonRow.html}
|
||||
${newsletterClosingRow.html}
|
||||
`),
|
||||
text: wrap.text(`
|
||||
${headingRow.text}
|
||||
${lead1Row.text}
|
||||
${buttonRow.text}
|
||||
${newsletterClosingRow.text}
|
||||
`),
|
||||
}
|
||||
|
||||
export const translations = { en, de, es, fr, nl, uk }
|
9
sites/backend/src/templates/email/nlsubinact/nl.json
Normal file
9
sites/backend/src/templates/email/nlsubinact/nl.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. (note that we already had a pending subscription for this address).",
|
||||
"greeting": "love,"
|
||||
}
|
9
sites/backend/src/templates/email/nlsubinact/uk.json
Normal file
9
sites/backend/src/templates/email/nlsubinact/uk.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Confirm your newsletter subscription",
|
||||
"heading": "Please confirm your newsletter subscription",
|
||||
"lead": "To confirm your subscription to the FreeSewing newsletter, click the big rectangle below:",
|
||||
"text-lead": "To confirm your subscription to the FreeSewing newsletter, click the link below:",
|
||||
"button": "Confirm subscription",
|
||||
"closing": "You (or someone else) tried to subscribe this email address to the FreeSewing newsletter just now. (note that we already had a pending subscription for this address).",
|
||||
"greeting": "love,"
|
||||
}
|
7
sites/backend/src/templates/email/passwordreset/de.json
Normal file
7
sites/backend/src/templates/email/passwordreset/de.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] Passwort zurücksetzen fixme",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] Passwort zurücksetzen fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
7
sites/backend/src/templates/email/passwordreset/en.json
Normal file
7
sites/backend/src/templates/email/passwordreset/en.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] passwordreset fixme",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] passwordreset fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
7
sites/backend/src/templates/email/passwordreset/es.json
Normal file
7
sites/backend/src/templates/email/passwordreset/es.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] passwordreset fixme",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] passwordreset fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
7
sites/backend/src/templates/email/passwordreset/fr.json
Normal file
7
sites/backend/src/templates/email/passwordreset/fr.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] passwordreset fixme",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] passwordreset fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -1,10 +1,11 @@
|
|||
import { buttonRow, closingRow, headingRow, 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' }
|
||||
import en from './en.json' assert { type: 'json' }
|
||||
import de from './de.json' assert { type: 'json' }
|
||||
import es from './es.json' assert { type: 'json' }
|
||||
import fr from './fr.json' assert { type: 'json' }
|
||||
import nl from './nl.json' assert { type: 'json' }
|
||||
import uk from './uk.json' assert { type: 'json' }
|
||||
|
||||
export const passwordreset = {
|
||||
html: wrap.html(`
|
||||
|
@ -34,4 +35,4 @@ ${closingRow.text}
|
|||
`),
|
||||
}
|
||||
|
||||
export const translations = { en, de, es, fr, nl }
|
||||
export const translations = { en, de, es, fr, nl, uk }
|
||||
|
|
7
sites/backend/src/templates/email/passwordreset/nl.json
Normal file
7
sites/backend/src/templates/email/passwordreset/nl.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] wachtwoord opnieuw instellen fixme",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] wachtwoord opnieuw instellen fixme'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
7
sites/backend/src/templates/email/passwordreset/uk.json
Normal file
7
sites/backend/src/templates/email/passwordreset/uk.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"subject": "[FreeSewing] виправлення скидання паролю",
|
||||
"heading": "FIXME",
|
||||
"lead": "fixme",
|
||||
"text-lead": "fixme",
|
||||
"closing": "fixme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
subject: '[FreeSewing] виправлення скидання паролю'
|
||||
heading: FIXME
|
||||
lead: fixme
|
||||
text-lead: fixme
|
||||
closing: fixme
|
|
@ -41,6 +41,32 @@ joost
|
|||
PS: {{{ text-ps }}} : {{{ supportUrl }}}`,
|
||||
}
|
||||
|
||||
export const newsletterClosingRow = {
|
||||
html: `
|
||||
<tr>
|
||||
<td align="left" class="sm-p-15px" style="padding-top: 30px">
|
||||
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #262626">
|
||||
{{ closing }}
|
||||
<br><br>
|
||||
{{ greeting }}
|
||||
<br>
|
||||
joost
|
||||
<br><br>
|
||||
PS: {{ ps-pre-link}}
|
||||
<a href="{{ psUrl }}" target="_blank" style="text-decoration: underline; color: #262626">
|
||||
<b>{{ ps-link}}</b>
|
||||
</a> {{ ps-post-link }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>`,
|
||||
text: `
|
||||
{{{ closing }}}
|
||||
|
||||
{{{ greeting }}}
|
||||
joost
|
||||
`,
|
||||
}
|
||||
|
||||
export const headingRow = {
|
||||
html: `
|
||||
<tr>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue