mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #2163 from gradido/1288-email-templates
feat(backend): 🍰 Email Templates
This commit is contained in:
commit
effa070f84
@ -1,7 +1,7 @@
|
|||||||
##################################################################################
|
##################################################################################
|
||||||
# BASE ###########################################################################
|
# BASE ###########################################################################
|
||||||
##################################################################################
|
##################################################################################
|
||||||
FROM node:12.19.0-alpine3.10 as base
|
FROM node:18.7.0-alpine3.16 as base
|
||||||
|
|
||||||
# ENVs (available in production aswell, can be overwritten by commandline or env file)
|
# ENVs (available in production aswell, can be overwritten by commandline or env file)
|
||||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||||
|
|||||||
@ -19,6 +19,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hyperswarm/dht": "^6.2.0",
|
"@hyperswarm/dht": "^6.2.0",
|
||||||
|
"@types/email-templates": "^10.0.1",
|
||||||
|
"@types/i18n": "^0.13.4",
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
@ -30,14 +32,17 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"decimal.js-light": "^2.5.1",
|
"decimal.js-light": "^2.5.1",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
|
"email-templates": "^10.0.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"graphql": "^15.5.1",
|
"graphql": "^15.5.1",
|
||||||
|
"i18n": "^0.15.1",
|
||||||
"jest": "^27.2.4",
|
"jest": "^27.2.4",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"log4js": "^6.4.6",
|
"log4js": "^6.4.6",
|
||||||
"mysql2": "^2.3.0",
|
"mysql2": "^2.3.0",
|
||||||
"nodemailer": "^6.6.5",
|
"nodemailer": "^6.6.5",
|
||||||
|
"pug": "^3.0.2",
|
||||||
"random-bigint": "^0.0.1",
|
"random-bigint": "^0.0.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sodium-native": "^3.3.0",
|
"sodium-native": "^3.3.0",
|
||||||
|
|||||||
50
backend/src/emails/README.md
Normal file
50
backend/src/emails/README.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Using `forwardemail`–`email-templates` With `pug` Package
|
||||||
|
|
||||||
|
You'll find the GitHub repository of the `email-templates` package and the `pug` package here:
|
||||||
|
|
||||||
|
- [email-templates](https://github.com/forwardemail/email-templates)
|
||||||
|
- [pug](https://www.npmjs.com/package/pug)
|
||||||
|
|
||||||
|
## `pug` Documentation
|
||||||
|
|
||||||
|
The full `pug` documentation you'll find here:
|
||||||
|
|
||||||
|
- [pugjs.org](https://pugjs.org/)
|
||||||
|
|
||||||
|
### Caching Possibility
|
||||||
|
|
||||||
|
In case we are sending many emails in the future there is the possibility to cache the `pug` templates:
|
||||||
|
|
||||||
|
- [cache-pug-templates](https://github.com/ladjs/cache-pug-templates)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To test your send emails you have different possibilities:
|
||||||
|
|
||||||
|
### In General
|
||||||
|
|
||||||
|
To send emails to yourself while developing set in `.env` the value `EMAIL_TEST_MODUS=true` and `EMAIL_TEST_RECEIVER` to your preferred email address.
|
||||||
|
|
||||||
|
### Unit Or Integration Tests
|
||||||
|
|
||||||
|
To change the behavior to show previews etc. you have the following options to be set in `sendEmailTranslated.ts` on creating the email object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const email = new Email({
|
||||||
|
…
|
||||||
|
// send emails in development/test env:
|
||||||
|
send: true,
|
||||||
|
…
|
||||||
|
// to open send emails in the browser
|
||||||
|
preview: true,
|
||||||
|
// or
|
||||||
|
// to open send emails in a specific the browser
|
||||||
|
preview: {
|
||||||
|
open: {
|
||||||
|
app: 'firefox',
|
||||||
|
wait: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
…
|
||||||
|
})
|
||||||
|
```
|
||||||
22
backend/src/emails/accountMultiRegistration/html.pug
Normal file
22
backend/src/emails/accountMultiRegistration/html.pug
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
doctype html
|
||||||
|
html(lang=locale)
|
||||||
|
head
|
||||||
|
title= t('emails.accountMultiRegistration.subject')
|
||||||
|
body
|
||||||
|
h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject')
|
||||||
|
#container.col
|
||||||
|
p(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.helloName', { firstName, lastName })
|
||||||
|
p= t('emails.accountMultiRegistration.emailReused')
|
||||||
|
br
|
||||||
|
span= t('emails.accountMultiRegistration.emailExists')
|
||||||
|
p= t('emails.accountMultiRegistration.onForgottenPasswordClickLink')
|
||||||
|
br
|
||||||
|
a(href=resendLink) #{resendLink}
|
||||||
|
br
|
||||||
|
span= t('emails.accountMultiRegistration.onForgottenPasswordCopyLink')
|
||||||
|
p= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
|
||||||
|
br
|
||||||
|
a(href='https://gradido.net/de/contact/') https://gradido.net/de/contact/
|
||||||
|
p(style='margin-top: 24px;')= t('emails.accountMultiRegistration.sincerelyYours')
|
||||||
|
br
|
||||||
|
span= t('emails.accountMultiRegistration.yourGradidoTeam')
|
||||||
1
backend/src/emails/accountMultiRegistration/subject.pug
Normal file
1
backend/src/emails/accountMultiRegistration/subject.pug
Normal file
@ -0,0 +1 @@
|
|||||||
|
= t('emails.accountMultiRegistration.subject')
|
||||||
110
backend/src/emails/sendEmailTranslated.test.ts
Normal file
110
backend/src/emails/sendEmailTranslated.test.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { createTransport } from 'nodemailer'
|
||||||
|
import { logger, i18n } from '@test/testSetup'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||||
|
|
||||||
|
CONFIG.EMAIL = false
|
||||||
|
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
|
||||||
|
CONFIG.EMAIL_SMTP_PORT = '1234'
|
||||||
|
CONFIG.EMAIL_USERNAME = 'user'
|
||||||
|
CONFIG.EMAIL_PASSWORD = 'pwd'
|
||||||
|
|
||||||
|
jest.mock('nodemailer', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
createTransport: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
sendMail: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
messageId: 'message',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sendEmailTranslated', () => {
|
||||||
|
let result: Record<string, unknown> | null
|
||||||
|
|
||||||
|
describe('config email is false', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
result = await sendEmailTranslated({
|
||||||
|
receiver: {
|
||||||
|
to: 'receiver@mail.org',
|
||||||
|
cc: 'support@gradido.net',
|
||||||
|
},
|
||||||
|
template: 'accountMultiRegistration',
|
||||||
|
locals: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs warning', () => {
|
||||||
|
expect(logger.info).toBeCalledWith('Emails are disabled via config...')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false', () => {
|
||||||
|
expect(result).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('config email is true', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
CONFIG.EMAIL = true
|
||||||
|
result = await sendEmailTranslated({
|
||||||
|
receiver: {
|
||||||
|
to: 'receiver@mail.org',
|
||||||
|
cc: 'support@gradido.net',
|
||||||
|
},
|
||||||
|
template: 'accountMultiRegistration',
|
||||||
|
locals: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the transporter', () => {
|
||||||
|
expect(createTransport).toBeCalledWith({
|
||||||
|
host: 'EMAIL_SMTP_URL',
|
||||||
|
port: 1234,
|
||||||
|
secure: false,
|
||||||
|
requireTLS: true,
|
||||||
|
auth: {
|
||||||
|
user: 'user',
|
||||||
|
pass: 'pwd',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('call of "sendEmailTranslated"', () => {
|
||||||
|
it('has expected result', () => {
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
envelope: {
|
||||||
|
from: 'info@gradido.net',
|
||||||
|
to: ['receiver@mail.org', 'support@gradido.net'],
|
||||||
|
},
|
||||||
|
message: expect.any(String),
|
||||||
|
originalMessage: expect.objectContaining({
|
||||||
|
to: 'receiver@mail.org',
|
||||||
|
cc: 'support@gradido.net',
|
||||||
|
from: 'Gradido (nicht antworten) <info@gradido.net>',
|
||||||
|
attachments: [],
|
||||||
|
subject: 'Gradido: Try To Register Again With Your Email',
|
||||||
|
html: expect.stringContaining('Gradido: Try To Register Again With Your Email'),
|
||||||
|
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls "i18n.setLocale" with "en"', () => {
|
||||||
|
expect(i18n.setLocale).toBeCalledWith('en')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls "i18n.__" for translation', () => {
|
||||||
|
expect(i18n.__).toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
85
backend/src/emails/sendEmailTranslated.ts
Normal file
85
backend/src/emails/sendEmailTranslated.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import path from 'path'
|
||||||
|
import { createTransport } from 'nodemailer'
|
||||||
|
import Email from 'email-templates'
|
||||||
|
import i18n from 'i18n'
|
||||||
|
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
|
export const sendEmailTranslated = async (params: {
|
||||||
|
receiver: {
|
||||||
|
to: string
|
||||||
|
cc?: string
|
||||||
|
}
|
||||||
|
template: string
|
||||||
|
locals: Record<string, string>
|
||||||
|
}): Promise<Record<string, unknown> | null> => {
|
||||||
|
let resultSend: Record<string, unknown> | null = null
|
||||||
|
|
||||||
|
// TODO: test the calling order of 'i18n.setLocale' for example: language of logging 'en', language of email receiver 'es', reset language of current user 'de'
|
||||||
|
|
||||||
|
// because language of receiver can differ from language of current user who triggers the sending
|
||||||
|
const rememberLocaleToRestore = i18n.getLocale()
|
||||||
|
|
||||||
|
i18n.setLocale('en') // for logging
|
||||||
|
logger.info(
|
||||||
|
`send Email: language=${params.locals.locale} to=${params.receiver.to}` +
|
||||||
|
(params.receiver.cc ? `, cc=${params.receiver.cc}` : '') +
|
||||||
|
`, subject=${i18n.__('emails.' + params.template + '.subject')}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!CONFIG.EMAIL) {
|
||||||
|
logger.info(`Emails are disabled via config...`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// because 'CONFIG.EMAIL_TEST_MODUS' can be boolean 'true' or string '`false`'
|
||||||
|
if (CONFIG.EMAIL_TEST_MODUS === true) {
|
||||||
|
logger.info(
|
||||||
|
`Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||||
|
)
|
||||||
|
params.receiver.to = CONFIG.EMAIL_TEST_RECEIVER
|
||||||
|
}
|
||||||
|
const transport = createTransport({
|
||||||
|
host: CONFIG.EMAIL_SMTP_URL,
|
||||||
|
port: Number(CONFIG.EMAIL_SMTP_PORT),
|
||||||
|
secure: false, // true for 465, false for other ports
|
||||||
|
requireTLS: true,
|
||||||
|
auth: {
|
||||||
|
user: CONFIG.EMAIL_USERNAME,
|
||||||
|
pass: CONFIG.EMAIL_PASSWORD,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
i18n.setLocale(params.locals.locale) // for email
|
||||||
|
|
||||||
|
// TESTING: see 'README.md'
|
||||||
|
const email = new Email({
|
||||||
|
message: {
|
||||||
|
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||||
|
},
|
||||||
|
transport,
|
||||||
|
preview: false,
|
||||||
|
// i18n, // is only needed if you don't install i18n
|
||||||
|
})
|
||||||
|
|
||||||
|
// ATTENTION: await is needed, because otherwise on send the email gets send in the language of the current user, because below the language gets reset
|
||||||
|
await email
|
||||||
|
.send({
|
||||||
|
template: path.join(__dirname, params.template),
|
||||||
|
message: params.receiver,
|
||||||
|
locals: params.locals, // the 'locale' in here seems not to be used by 'email-template', because it doesn't work if the language isn't set before by 'i18n.setLocale'
|
||||||
|
})
|
||||||
|
.then((result: Record<string, unknown>) => {
|
||||||
|
resultSend = result
|
||||||
|
logger.info('Send email successfully !!!')
|
||||||
|
logger.info('Result: ', result)
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
logger.error('Error sending notification email: ', error)
|
||||||
|
throw new Error('Error sending notification email!')
|
||||||
|
})
|
||||||
|
|
||||||
|
i18n.setLocale(rememberLocaleToRestore)
|
||||||
|
|
||||||
|
return resultSend
|
||||||
|
}
|
||||||
88
backend/src/emails/sendEmailVariants.test.ts
Normal file
88
backend/src/emails/sendEmailVariants.test.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import CONFIG from '@/config'
|
||||||
|
import { sendAccountMultiRegistrationEmail } from './sendEmailVariants'
|
||||||
|
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||||
|
|
||||||
|
CONFIG.EMAIL = true
|
||||||
|
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
|
||||||
|
CONFIG.EMAIL_SMTP_PORT = '1234'
|
||||||
|
CONFIG.EMAIL_USERNAME = 'user'
|
||||||
|
CONFIG.EMAIL_PASSWORD = 'pwd'
|
||||||
|
|
||||||
|
jest.mock('./sendEmailTranslated', () => {
|
||||||
|
const originalModule = jest.requireActual('./sendEmailTranslated')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
sendEmailTranslated: jest.fn((a) => originalModule.sendEmailTranslated(a)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sendEmailVariants', () => {
|
||||||
|
let result: Record<string, unknown> | null
|
||||||
|
|
||||||
|
describe('sendAccountMultiRegistrationEmail', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
result = await sendAccountMultiRegistrationEmail({
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Lustig',
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
language: 'en',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('calls "sendEmailTranslated"', () => {
|
||||||
|
it('with expected parameters', () => {
|
||||||
|
expect(sendEmailTranslated).toBeCalledWith({
|
||||||
|
receiver: {
|
||||||
|
to: 'Peter Lustig <peter@lustig.de>',
|
||||||
|
},
|
||||||
|
template: 'accountMultiRegistration',
|
||||||
|
locals: {
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Lustig',
|
||||||
|
locale: 'en',
|
||||||
|
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has expected result', () => {
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
envelope: {
|
||||||
|
from: 'info@gradido.net',
|
||||||
|
to: ['peter@lustig.de'],
|
||||||
|
},
|
||||||
|
message: expect.any(String),
|
||||||
|
originalMessage: expect.objectContaining({
|
||||||
|
to: 'Peter Lustig <peter@lustig.de>',
|
||||||
|
from: 'Gradido (nicht antworten) <info@gradido.net>',
|
||||||
|
attachments: [],
|
||||||
|
subject: 'Gradido: Try To Register Again With Your Email',
|
||||||
|
html:
|
||||||
|
expect.stringContaining(
|
||||||
|
'<title>Gradido: Try To Register Again With Your Email</title>',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('>Gradido: Try To Register Again With Your Email</h1>') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'Your email address has just been used again to register an account with Gradido.',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'However, an account already exists for your email address.',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'Please click on the following link if you have forgotten your password:',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining(
|
||||||
|
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('or copy the link above into your browser window.') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'If you are not the one who tried to register again, please contact our support:',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('Sincerely yours,<br><span>your Gradido team'),
|
||||||
|
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
20
backend/src/emails/sendEmailVariants.ts
Normal file
20
backend/src/emails/sendEmailVariants.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import CONFIG from '@/config'
|
||||||
|
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||||
|
|
||||||
|
export const sendAccountMultiRegistrationEmail = (data: {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
language: string
|
||||||
|
}): Promise<Record<string, unknown> | null> => {
|
||||||
|
return sendEmailTranslated({
|
||||||
|
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||||
|
template: 'accountMultiRegistration',
|
||||||
|
locals: {
|
||||||
|
locale: data.language,
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ import { GraphQLError } from 'graphql'
|
|||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants'
|
||||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { printTimeDuration, activationLink } from './UserResolver'
|
import { printTimeDuration, activationLink } from './UserResolver'
|
||||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||||
@ -29,7 +29,7 @@ import { TransactionLink } from '@entity/TransactionLink'
|
|||||||
|
|
||||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||||
import { EventProtocol } from '@entity/EventProtocol'
|
import { EventProtocol } from '@entity/EventProtocol'
|
||||||
import { logger } from '@test/testSetup'
|
import { logger, i18n as localization } from '@test/testSetup'
|
||||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
import { UserContact } from '@entity/UserContact'
|
import { UserContact } from '@entity/UserContact'
|
||||||
@ -46,7 +46,7 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.mock('@/mailer/sendAccountMultiRegistrationEmail', () => {
|
jest.mock('@/emails/sendEmailVariants', () => {
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
sendAccountMultiRegistrationEmail: jest.fn(),
|
sendAccountMultiRegistrationEmail: jest.fn(),
|
||||||
@ -73,7 +73,7 @@ let mutate: any, query: any, con: any
|
|||||||
let testEnv: any
|
let testEnv: any
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
testEnv = await testEnvironment(logger)
|
testEnv = await testEnvironment(logger, localization)
|
||||||
mutate = testEnv.mutate
|
mutate = testEnv.mutate
|
||||||
query = testEnv.query
|
query = testEnv.query
|
||||||
con = testEnv.con
|
con = testEnv.con
|
||||||
@ -213,6 +213,7 @@ describe('UserResolver', () => {
|
|||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
|
language: 'de',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import i18n from 'i18n'
|
||||||
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
|
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
|
||||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||||
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
||||||
@ -18,7 +19,7 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
|
|||||||
import { OptInType } from '@enum/OptInType'
|
import { OptInType } from '@enum/OptInType'
|
||||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants'
|
||||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
@ -358,6 +359,8 @@ export class UserResolver {
|
|||||||
const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset))
|
const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset))
|
||||||
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
||||||
|
|
||||||
|
i18n.setLocale(user.language)
|
||||||
|
|
||||||
// Elopage Status & Stored PublisherId
|
// Elopage Status & Stored PublisherId
|
||||||
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||||
logger.info('user.hasElopage=' + user.hasElopage)
|
logger.info('user.hasElopage=' + user.hasElopage)
|
||||||
@ -410,6 +413,7 @@ export class UserResolver {
|
|||||||
if (!language || !isLanguage(language)) {
|
if (!language || !isLanguage(language)) {
|
||||||
language = DEFAULT_LANGUAGE
|
language = DEFAULT_LANGUAGE
|
||||||
}
|
}
|
||||||
|
i18n.setLocale(language)
|
||||||
|
|
||||||
// check if user with email still exists?
|
// check if user with email still exists?
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
@ -418,8 +422,11 @@ export class UserResolver {
|
|||||||
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
||||||
|
|
||||||
if (foundUser) {
|
if (foundUser) {
|
||||||
// ATTENTION: this logger-message will be exactly expected during tests
|
// ATTENTION: this logger-message will be exactly expected during tests, next line
|
||||||
logger.info(`User already exists with this email=${email}`)
|
logger.info(`User already exists with this email=${email}`)
|
||||||
|
logger.info(
|
||||||
|
`Specified username when trying to register multiple times with this email: firstName=${firstName}, lastName=${lastName}`,
|
||||||
|
)
|
||||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||||
|
|
||||||
const user = new User(communityDbUser)
|
const user = new User(communityDbUser)
|
||||||
@ -432,18 +439,20 @@ export class UserResolver {
|
|||||||
user.publisherId = publisherId
|
user.publisherId = publisherId
|
||||||
logger.debug('partly faked user=' + user)
|
logger.debug('partly faked user=' + user)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||||
firstName,
|
firstName: foundUser.firstName, // this is the real name of the email owner, but just "firstName" would be the name of the new registrant which shall not be passed to the outside
|
||||||
lastName,
|
lastName: foundUser.lastName, // this is the real name of the email owner, but just "lastName" would be the name of the new registrant which shall not be passed to the outside
|
||||||
email,
|
email,
|
||||||
|
language: foundUser.language, // use language of the emails owner for sending
|
||||||
})
|
})
|
||||||
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||||
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||||
eventProtocol.writeEvent(
|
eventProtocol.writeEvent(
|
||||||
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
||||||
)
|
)
|
||||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
logger.info(
|
||||||
|
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
|
||||||
|
)
|
||||||
/* uncomment this, when you need the activation link on the console */
|
/* uncomment this, when you need the activation link on the console */
|
||||||
// In case EMails are disabled log the activation link for the user
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
@ -787,6 +796,7 @@ export class UserResolver {
|
|||||||
throw new Error(`"${language}" isn't a valid language`)
|
throw new Error(`"${language}" isn't a valid language`)
|
||||||
}
|
}
|
||||||
userEntity.language = language
|
userEntity.language = language
|
||||||
|
i18n.setLocale(language)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password && passwordNew) {
|
if (password && passwordNew) {
|
||||||
|
|||||||
15
backend/src/locales/de.json
Normal file
15
backend/src/locales/de.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"emails": {
|
||||||
|
"accountMultiRegistration": {
|
||||||
|
"emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.",
|
||||||
|
"emailReused": "Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.",
|
||||||
|
"helloName": "Hallo {firstName} {lastName}",
|
||||||
|
"ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:",
|
||||||
|
"onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:",
|
||||||
|
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
|
||||||
|
"sincerelyYours": "Mit freundlichen Grüßen,",
|
||||||
|
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail",
|
||||||
|
"yourGradidoTeam": "dein Gradido-Team"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
backend/src/locales/en.json
Normal file
15
backend/src/locales/en.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"emails": {
|
||||||
|
"accountMultiRegistration": {
|
||||||
|
"emailExists": "However, an account already exists for your email address.",
|
||||||
|
"emailReused": "Your email address has just been used again to register an account with Gradido.",
|
||||||
|
"helloName": "Hello {firstName} {lastName}",
|
||||||
|
"ifYouAreNotTheOne": "If you are not the one who tried to register again, please contact our support:",
|
||||||
|
"onForgottenPasswordClickLink": "Please click on the following link if you have forgotten your password:",
|
||||||
|
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
|
||||||
|
"sincerelyYours": "Sincerely yours,",
|
||||||
|
"subject": "Gradido: Try To Register Again With Your Email",
|
||||||
|
"yourGradidoTeam": "your Gradido team"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import CONFIG from '@/config'
|
|
||||||
import { sendAccountMultiRegistrationEmail } from './sendAccountMultiRegistrationEmail'
|
|
||||||
import { sendEMail } from './sendEMail'
|
|
||||||
|
|
||||||
jest.mock('./sendEMail', () => {
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
sendEMail: jest.fn(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('sendAccountMultiRegistrationEmail', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await sendAccountMultiRegistrationEmail({
|
|
||||||
firstName: 'Peter',
|
|
||||||
lastName: 'Lustig',
|
|
||||||
email: 'peter@lustig.de',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls sendEMail', () => {
|
|
||||||
expect(sendEMail).toBeCalledWith({
|
|
||||||
to: `Peter Lustig <peter@lustig.de>`,
|
|
||||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
|
||||||
text:
|
|
||||||
expect.stringContaining('Hallo Peter Lustig') &&
|
|
||||||
expect.stringContaining(CONFIG.EMAIL_LINK_FORGOTPASSWORD) &&
|
|
||||||
expect.stringContaining('https://gradido.net/de/contact/'),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { sendEMail } from './sendEMail'
|
|
||||||
import { accountMultiRegistration } from './text/accountMultiRegistration'
|
|
||||||
import CONFIG from '@/config'
|
|
||||||
|
|
||||||
export const sendAccountMultiRegistrationEmail = (data: {
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
email: string
|
|
||||||
}): Promise<boolean> => {
|
|
||||||
return sendEMail({
|
|
||||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
|
||||||
subject: accountMultiRegistration.de.subject,
|
|
||||||
text: accountMultiRegistration.de.text({
|
|
||||||
...data,
|
|
||||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -38,7 +38,7 @@ describe('sendEMail', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs warining', () => {
|
it('logs warning', () => {
|
||||||
expect(logger.info).toBeCalledWith('Emails are disabled via config...')
|
expect(logger.info).toBeCalledWith('Emails are disabled via config...')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,9 @@ import { Connection } from '@dbTools/typeorm'
|
|||||||
import { apolloLogger } from './logger'
|
import { apolloLogger } from './logger'
|
||||||
import { Logger } from 'log4js'
|
import { Logger } from 'log4js'
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
import { i18n } from './localization'
|
||||||
|
|
||||||
// TODO implement
|
// TODO implement
|
||||||
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||||
|
|
||||||
@ -34,6 +37,7 @@ const createServer = async (
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
context: any = serverContext,
|
context: any = serverContext,
|
||||||
logger: Logger = apolloLogger,
|
logger: Logger = apolloLogger,
|
||||||
|
localization: i18n.I18n = i18n,
|
||||||
): Promise<ServerDef> => {
|
): Promise<ServerDef> => {
|
||||||
logger.addContext('user', 'unknown')
|
logger.addContext('user', 'unknown')
|
||||||
logger.debug('createServer...')
|
logger.debug('createServer...')
|
||||||
@ -63,6 +67,9 @@ const createServer = async (
|
|||||||
// bodyparser urlencoded for elopage
|
// bodyparser urlencoded for elopage
|
||||||
app.use(express.urlencoded({ extended: true }))
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
app.use(localization.init)
|
||||||
|
|
||||||
// Elopage Webhook
|
// Elopage Webhook
|
||||||
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||||
|
|
||||||
@ -80,6 +87,7 @@ const createServer = async (
|
|||||||
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
|
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
|
||||||
)
|
)
|
||||||
logger.debug('createServer...successful')
|
logger.debug('createServer...successful')
|
||||||
|
|
||||||
return { apollo, app, con }
|
return { apollo, app, con }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
backend/src/server/localization.ts
Normal file
28
backend/src/server/localization.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { backendLogger } from './logger'
|
||||||
|
import i18n from 'i18n'
|
||||||
|
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['en', 'de'],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
retryInDefaultLocale: false,
|
||||||
|
directory: path.join(__dirname, '..', 'locales'),
|
||||||
|
// autoReload: true, // if this is activated the seeding hangs at the very end
|
||||||
|
updateFiles: false,
|
||||||
|
objectNotation: true,
|
||||||
|
logDebugFn: (msg) => backendLogger.debug(msg),
|
||||||
|
logWarnFn: (msg) => backendLogger.info(msg),
|
||||||
|
logErrorFn: (msg) => backendLogger.error(msg),
|
||||||
|
// this api is needed for email-template pug files
|
||||||
|
api: {
|
||||||
|
__: 't', // now req.__ becomes req.t
|
||||||
|
__n: 'tn', // and req.__n can be called as req.tn
|
||||||
|
},
|
||||||
|
register: global,
|
||||||
|
mustacheConfig: {
|
||||||
|
tags: ['{', '}'],
|
||||||
|
disable: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export { i18n }
|
||||||
@ -26,8 +26,8 @@ export const cleanDB = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const testEnvironment = async (logger?: any) => {
|
export const testEnvironment = async (logger?: any, localization?: any) => {
|
||||||
const server = await createServer(context, logger)
|
const server = await createServer(context, logger, localization)
|
||||||
const con = server.con
|
const con = server.con
|
||||||
const testClient = createTestClient(server.apollo)
|
const testClient = createTestClient(server.apollo)
|
||||||
const mutate = testClient.mutate
|
const mutate = testClient.mutate
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import { i18n } from '@/server/localization'
|
||||||
|
|
||||||
jest.setTimeout(1000000)
|
jest.setTimeout(1000000)
|
||||||
|
|
||||||
@ -19,4 +20,18 @@ jest.mock('@/server/logger', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export { logger }
|
jest.mock('@/server/localization', () => {
|
||||||
|
const originalModule = jest.requireActual('@/server/localization')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...originalModule,
|
||||||
|
i18n: {
|
||||||
|
init: jest.fn(),
|
||||||
|
// configure: jest.fn(),
|
||||||
|
// __: jest.fn(),
|
||||||
|
// setLocale: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { logger, i18n }
|
||||||
|
|||||||
1027
backend/yarn.lock
1027
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user