mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into clear-old-password-junk
This commit is contained in:
commit
35f9157f41
@ -1,10 +1,28 @@
|
||||
# Docker More Closely
|
||||
# Contributing
|
||||
|
||||
## Apple M1 Platform
|
||||
If you contribute to our project, please consider the following points.
|
||||
|
||||
## Localization
|
||||
|
||||
### Quotation Marks
|
||||
|
||||
The following characters are different from the programming quotation mark:
|
||||
|
||||
`"` or `\"`
|
||||
|
||||
Please copy and paste the following quotes for the languages:
|
||||
|
||||
- de: „Dies ist ein Beispielsatz.“
|
||||
- en: “This is a sample sentence.”
|
||||
- See <https://grammar.collinsdictionary.com/easy-learning/when-do-you-use-quotation-marks-or-in-english>
|
||||
|
||||
## Docker – More Closely
|
||||
|
||||
### Apple M1 Platform
|
||||
|
||||
***Attention:** For using Docker commands in Apple M1 environments!*
|
||||
|
||||
### Enviroment Variable For Apple M1 Platform
|
||||
#### Environment Variable For Apple M1 Platform
|
||||
|
||||
To set the Docker platform environment variable in your terminal tab, run:
|
||||
|
||||
@ -13,7 +31,7 @@ To set the Docker platform environment variable in your terminal tab, run:
|
||||
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
```
|
||||
|
||||
### Docker Compose Override File For Apple M1 Platform
|
||||
#### Docker Compose Override File For Apple M1 Platform
|
||||
|
||||
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
|
||||
|
||||
@ -27,7 +45,7 @@ $ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
|
||||
```
|
||||
|
||||
## Analysing Docker Builds
|
||||
### Analyzing Docker Builds
|
||||
|
||||
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
|
||||
|
||||
@ -38,10 +38,12 @@ export default {
|
||||
form: {
|
||||
text: '',
|
||||
},
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit(event) {
|
||||
this.loading = true
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: adminCreateContributionMessage,
|
||||
@ -55,9 +57,11 @@ export default {
|
||||
this.$emit('update-state', this.contributionId)
|
||||
this.form.text = ''
|
||||
this.toastSuccess(this.$t('message.request'))
|
||||
this.loading = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
onReset(event) {
|
||||
@ -66,10 +70,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
disabled() {
|
||||
if (this.form.text !== '') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return this.form.text === '' || this.loading
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -98,10 +98,18 @@ COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
|
||||
# We also copy the node_modules express and serve-static for the run script
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules
|
||||
|
||||
# Copy static files
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
|
||||
# Copy package.json for script definitions (lock file should not be needed)
|
||||
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
|
||||
# Copy tsconfig.json to provide alias path definitions
|
||||
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
|
||||
# Copy log4js-config.json to provide log configuration
|
||||
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json
|
||||
# Copy memonic type since its referenced in the sources
|
||||
# TODO: remove
|
||||
COPY --from=build ${DOCKER_WORKDIR}/src/config/mnemonic.uncompressed_buffer13116.txt ./src/config/mnemonic.uncompressed_buffer13116.txt
|
||||
# Copy run scripts run/
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"build": "tsc --build && mkdir -p build/src/emails/templates/ && cp -r src/emails/templates/* build/src/emails/templates/ && mkdir -p build/src/locales/ && cp -r src/locales/*.json build/src/locales/",
|
||||
"clean": "tsc --build --clean",
|
||||
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
||||
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
|
||||
|
||||
@ -10,7 +10,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0055-clear_old_password_junk',
|
||||
DB_VERSION: '0057-clear_old_password_junk',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -1 +0,0 @@
|
||||
= t('emails.accountMultiRegistration.subject')
|
||||
@ -89,7 +89,7 @@ describe('sendEmailTranslated', () => {
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
from: 'Gradido (nicht antworten) <info@gradido.net>',
|
||||
from: 'Gradido (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Try To Register Again With Your Email',
|
||||
html: expect.stringContaining('Gradido: Try To Register Again With Your Email'),
|
||||
@ -107,4 +107,41 @@ describe('sendEmailTranslated', () => {
|
||||
expect(i18n.__).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with email EMAIL_TEST_MODUS true', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
CONFIG.EMAIL = true
|
||||
CONFIG.EMAIL_TEST_MODUS = true
|
||||
result = await sendEmailTranslated({
|
||||
receiver: {
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
},
|
||||
template: 'accountMultiRegistration',
|
||||
locals: {
|
||||
locale: 'en',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('call of "sendEmailTranslated" with faked "to"', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: CONFIG.EMAIL_SENDER,
|
||||
to: [CONFIG.EMAIL_TEST_RECEIVER, 'support@gradido.net'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: CONFIG.EMAIL_TEST_RECEIVER,
|
||||
cc: 'support@gradido.net',
|
||||
from: `Gradido (do not answer) <${CONFIG.EMAIL_SENDER}>`,
|
||||
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'),
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
import CONFIG from '@/config'
|
||||
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>
|
||||
locals: Record<string, unknown>
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
let resultSend: Record<string, unknown> | null = null
|
||||
|
||||
@ -32,8 +31,7 @@ export const sendEmailTranslated = async (params: {
|
||||
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) {
|
||||
if (CONFIG.EMAIL_TEST_MODUS) {
|
||||
logger.info(
|
||||
`Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||
)
|
||||
@ -50,12 +48,12 @@ export const sendEmailTranslated = async (params: {
|
||||
},
|
||||
})
|
||||
|
||||
i18n.setLocale(params.locals.locale) // for email
|
||||
i18n.setLocale(params.locals.locale as string) // for email
|
||||
|
||||
// TESTING: see 'README.md'
|
||||
const email = new Email({
|
||||
message: {
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
from: `Gradido (${i18n.__('emails.general.doNotAnswer')}) <${CONFIG.EMAIL_SENDER}>`,
|
||||
},
|
||||
transport,
|
||||
preview: false,
|
||||
@ -65,7 +63,7 @@ export const sendEmailTranslated = async (params: {
|
||||
// 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),
|
||||
template: path.join(__dirname, 'templates', 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'
|
||||
})
|
||||
|
||||
@ -1,12 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { testEnvironment } from '@test/helpers'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountMultiRegistrationEmail } from './sendEmailVariants'
|
||||
import {
|
||||
sendAddedContributionMessageEmail,
|
||||
sendAccountActivationEmail,
|
||||
sendAccountMultiRegistrationEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionRejectedEmail,
|
||||
sendResetPasswordEmail,
|
||||
sendTransactionLinkRedeemedEmail,
|
||||
sendTransactionReceivedEmail,
|
||||
} 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'
|
||||
let con: any
|
||||
let testEnv: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger, localization)
|
||||
con = testEnv.con
|
||||
// await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
jest.mock('./sendEmailTranslated', () => {
|
||||
const originalModule = jest.requireActual('./sendEmailTranslated')
|
||||
@ -17,7 +39,154 @@ jest.mock('./sendEmailTranslated', () => {
|
||||
})
|
||||
|
||||
describe('sendEmailVariants', () => {
|
||||
let result: Record<string, unknown> | null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let result: any
|
||||
|
||||
describe('sendAddedContributionMessageEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendAddedContributionMessageEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
})
|
||||
})
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'addedContributionMessage',
|
||||
locals: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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 (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Message about your common good contribution',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: MESSAGE ABOUT YOUR COMMON GOOD CONTRIBUTION'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: Message about your common good contribution</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Message about your common good contribution</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'you have received a message from Bibi Bloxberg regarding your common good contribution “My contribution.”.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`Link to your account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendAccountActivationEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendAccountActivationEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
language: 'en',
|
||||
activationLink: 'http://localhost/checkEmail/6627633878930542284',
|
||||
timeDurationObject: { hours: 23, minutes: 30 },
|
||||
})
|
||||
})
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'accountActivation',
|
||||
locals: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
activationLink: 'http://localhost/checkEmail/6627633878930542284',
|
||||
timeDurationObject: { hours: 23, minutes: 30 },
|
||||
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 (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Email Verification',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain('<title>Gradido: Email Verification</title>')
|
||||
expect(result.originalMessage.html).toContain('>Gradido: Email Verification</h1>')
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'Your email address has just been registered with Gradido.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'Please click on this link to complete the registration and activate your Gradido account:',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<a href="http://localhost/checkEmail/6627633878930542284">http://localhost/checkEmail/6627633878930542284</a>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'or copy the link above into your browser window.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendAccountMultiRegistrationEmail', () => {
|
||||
beforeAll(async () => {
|
||||
@ -54,34 +223,401 @@ describe('sendEmailVariants', () => {
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (nicht antworten) <info@gradido.net>',
|
||||
from: 'Gradido (do not answer) <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'),
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: Try To Register Again With Your Email</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Try To Register Again With Your Email</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'Your email address has just been used again to register an account with Gradido.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'However, an account already exists for your email address.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'Please click on the following link if you have forgotten your password:',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'or copy the link above into your browser window.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'If you are not the one who tried to register again, please contact our support:',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendContributionConfirmedEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendContributionConfirmedEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
contributionAmount: new Decimal(23.54),
|
||||
})
|
||||
})
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'contributionConfirmed',
|
||||
locals: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
contributionAmount: '23.54',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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 (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Your common good contribution was confirmed',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS CONFIRMED'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: Your common good contribution was confirmed</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Your common good contribution was confirmed</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'Your public good contribution “My contribution.” has just been confirmed by Bibi Bloxberg and credited to your Gradido account.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Amount: 23.54 GDD')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`Link to your account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendContributionRejectedEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendContributionRejectedEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
})
|
||||
})
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'contributionRejected',
|
||||
locals: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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 (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Your common good contribution was rejected',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS REJECTED'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: Your common good contribution was rejected</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Your common good contribution was rejected</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'Your public good contribution “My contribution.” was rejected by Bibi Bloxberg.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`Link to your account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendResetPasswordEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendResetPasswordEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
language: 'en',
|
||||
resetLink: 'http://localhost/reset-password/3762660021544901417',
|
||||
timeDurationObject: { hours: 23, minutes: 30 },
|
||||
})
|
||||
})
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'resetPassword',
|
||||
locals: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
resetLink: 'http://localhost/reset-password/3762660021544901417',
|
||||
timeDurationObject: { hours: 23, minutes: 30 },
|
||||
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 (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Reset password',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: RESET PASSWORD'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain('<title>Gradido: Reset password</title>')
|
||||
expect(result.originalMessage.html).toContain('>Gradido: Reset password</h1>')
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'You, or someone else, requested a password reset for this account.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('If it was you, please click on the link:')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<a href="http://localhost/reset-password/3762660021544901417">http://localhost/reset-password/3762660021544901417</a>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'or copy the link above into your browser window.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendTransactionLinkRedeemedEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendTransactionLinkRedeemedEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
transactionMemo: 'You deserve it! 🙏🏼',
|
||||
transactionAmount: new Decimal(17.65),
|
||||
})
|
||||
})
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'transactionLinkRedeemed',
|
||||
locals: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
transactionMemo: 'You deserve it! 🙏🏼',
|
||||
transactionAmount: '17.65',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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 (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Your Gradido link has been redeemed',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: YOUR GRADIDO LINK HAS BEEN REDEEMED'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: Your Gradido link has been redeemed</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Your Gradido link has been redeemed</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'Bibi Bloxberg (bibi@bloxberg.de) has just redeemed your link.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Amount: 17.65 GDD')
|
||||
expect(result.originalMessage.html).toContain('Memo: You deserve it! 🙏🏼')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`You can find transaction details in your Gradido account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendTransactionReceivedEmail', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendTransactionReceivedEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
language: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
transactionAmount: new Decimal(37.4),
|
||||
})
|
||||
})
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
template: 'transactionReceived',
|
||||
locals: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
transactionAmount: '37.40',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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 (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: You have received Gradidos',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: YOU HAVE RECEIVED GRADIDOS'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: You have received Gradidos</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('>Gradido: You have received Gradidos</h1>')
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`You can find transaction details in your Gradido account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,56 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import CONFIG from '@/config'
|
||||
import { decimalSeparatorByLanguage } from '@/util/utilities'
|
||||
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||
|
||||
export const sendAddedContributionMessageEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
contributionMemo: string
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: {
|
||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||
},
|
||||
template: 'addedContributionMessage',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
contributionMemo: data.contributionMemo,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendAccountActivationEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
activationLink: string
|
||||
timeDurationObject: Record<string, unknown>
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'accountActivation',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
activationLink: data.activationLink,
|
||||
timeDurationObject: data.timeDurationObject,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendAccountMultiRegistrationEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
@ -11,10 +61,136 @@ export const sendAccountMultiRegistrationEmail = (data: {
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'accountMultiRegistration',
|
||||
locals: {
|
||||
locale: data.language,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendContributionConfirmedEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
contributionMemo: string
|
||||
contributionAmount: Decimal
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'contributionConfirmed',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
contributionMemo: data.contributionMemo,
|
||||
contributionAmount: decimalSeparatorByLanguage(data.contributionAmount, data.language),
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendContributionRejectedEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
contributionMemo: string
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'contributionRejected',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
contributionMemo: data.contributionMemo,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendResetPasswordEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
resetLink: string
|
||||
timeDurationObject: Record<string, unknown>
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'resetPassword',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
resetLink: data.resetLink,
|
||||
timeDurationObject: data.timeDurationObject,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendTransactionLinkRedeemedEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
senderEmail: string
|
||||
transactionMemo: string
|
||||
transactionAmount: Decimal
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'transactionLinkRedeemed',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
senderEmail: data.senderEmail,
|
||||
transactionMemo: data.transactionMemo,
|
||||
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendTransactionReceivedEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
senderEmail: string
|
||||
transactionAmount: Decimal
|
||||
}): Promise<Record<string, unknown> | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'transactionReceived',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
senderEmail: data.senderEmail,
|
||||
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
20
backend/src/emails/templates/accountActivation/html.pug
Normal file
20
backend/src/emails/templates/accountActivation/html.pug
Normal file
@ -0,0 +1,20 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.accountActivation.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.accountActivation.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.accountActivation.emailRegistered')
|
||||
p= t('emails.accountActivation.pleaseClickLink')
|
||||
br
|
||||
a(href=activationLink) #{activationLink}
|
||||
br
|
||||
span= t('emails.general.orCopyLink')
|
||||
p= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
|
||||
br
|
||||
a(href=resendLink) #{resendLink}
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.accountActivation.subject')
|
||||
@ -5,7 +5,7 @@ html(lang=locale)
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.helloName', { firstName, lastName })
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.accountMultiRegistration.emailReused')
|
||||
br
|
||||
span= t('emails.accountMultiRegistration.emailExists')
|
||||
@ -17,6 +17,6 @@ html(lang=locale)
|
||||
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')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.accountMultiRegistration.yourGradidoTeam')
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.accountMultiRegistration.subject')
|
||||
@ -0,0 +1,17 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.addedContributionMessage.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.addedContributionMessage.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo })
|
||||
p= t('emails.addedContributionMessage.toSeeAndAnswerMessage')
|
||||
p= t('emails.general.linkToYourAccount')
|
||||
span= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.addedContributionMessage.subject')
|
||||
17
backend/src/emails/templates/contributionConfirmed/html.pug
Normal file
17
backend/src/emails/templates/contributionConfirmed/html.pug
Normal file
@ -0,0 +1,17 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.contributionConfirmed.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.contributionConfirmed.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo })
|
||||
p= t('emails.general.amountGDD', { amountGDD: contributionAmount })
|
||||
p= t('emails.general.linkToYourAccount')
|
||||
span= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.contributionConfirmed.subject')
|
||||
17
backend/src/emails/templates/contributionRejected/html.pug
Normal file
17
backend/src/emails/templates/contributionRejected/html.pug
Normal file
@ -0,0 +1,17 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.contributionRejected.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.contributionRejected.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.contributionRejected.commonGoodContributionRejected', { senderFirstName, senderLastName, contributionMemo })
|
||||
p= t('emails.contributionRejected.toSeeContributionsAndMessages')
|
||||
p= t('emails.general.linkToYourAccount')
|
||||
span= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.contributionRejected.subject')
|
||||
20
backend/src/emails/templates/resetPassword/html.pug
Normal file
20
backend/src/emails/templates/resetPassword/html.pug
Normal file
@ -0,0 +1,20 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.resetPassword.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.resetPassword.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.resetPassword.youOrSomeoneResetPassword')
|
||||
p= t('emails.resetPassword.pleaseClickLink')
|
||||
br
|
||||
a(href=resetLink) #{resetLink}
|
||||
br
|
||||
span= t('emails.general.orCopyLink')
|
||||
p= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
|
||||
br
|
||||
a(href=resendLink) #{resendLink}
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
1
backend/src/emails/templates/resetPassword/subject.pug
Normal file
1
backend/src/emails/templates/resetPassword/subject.pug
Normal file
@ -0,0 +1 @@
|
||||
= t('emails.resetPassword.subject')
|
||||
@ -0,0 +1,19 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.transactionLinkRedeemed.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.transactionLinkRedeemed.hasRedeemedYourLink', { senderFirstName, senderLastName, senderEmail })
|
||||
p= t('emails.general.amountGDD', { amountGDD: transactionAmount })
|
||||
br
|
||||
span= t('emails.transactionLinkRedeemed.memo', { transactionMemo })
|
||||
p= t('emails.general.detailsYouFindOnLinkToYourAccount')
|
||||
span= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.transactionLinkRedeemed.subject')
|
||||
16
backend/src/emails/templates/transactionReceived/html.pug
Normal file
16
backend/src/emails/templates/transactionReceived/html.pug
Normal file
@ -0,0 +1,16 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.transactionReceived.subject')
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail })
|
||||
p= t('emails.general.detailsYouFindOnLinkToYourAccount')
|
||||
span= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.transactionReceived.subject')
|
||||
@ -42,7 +42,9 @@ export class Transaction {
|
||||
this.creationDate = transaction.creationDate
|
||||
this.linkedUser = linkedUser
|
||||
this.linkedTransactionId = transaction.linkedTransactionId
|
||||
this.transactionLinkId = transaction.transactionLinkId
|
||||
this.linkId = transaction.contribution
|
||||
? transaction.contribution.contributionLinkId
|
||||
: transaction.transactionLinkId
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
@ -81,7 +83,7 @@ export class Transaction {
|
||||
@Field(() => Number, { nullable: true })
|
||||
linkedTransactionId?: number | null
|
||||
|
||||
// Links to the TransactionLink when transaction was created by a link
|
||||
// Links to the TransactionLink/ContributionLink when transaction was created by a link
|
||||
@Field(() => Number, { nullable: true })
|
||||
transactionLinkId?: number | null
|
||||
linkId?: number | null
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
import { objectValuesToArray } from '@/util/utilities'
|
||||
import { testEnvironment, resetToken, cleanDB, contributionDateFormatter } from '@test/helpers'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { creationFactory } from '@/seeds/factory/creation'
|
||||
import { creations } from '@/seeds/creation/index'
|
||||
@ -35,30 +36,31 @@ import {
|
||||
} from '@/seeds/graphql/queries'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { User } from '@entity/User'
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import {
|
||||
// sendAccountActivationEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
// sendContributionRejectedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
// mock account activation email to avoid console spam
|
||||
jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
return {
|
||||
__esModule: true,
|
||||
sendAccountActivationEmail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
// mock account activation email to avoid console spam
|
||||
jest.mock('@/mailer/sendContributionConfirmedEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendContributionConfirmedEmail: jest.fn(),
|
||||
...originalModule,
|
||||
// TODO: test the call of …
|
||||
// sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)),
|
||||
sendContributionConfirmedEmail: jest.fn((a) =>
|
||||
originalModule.sendContributionConfirmedEmail(a),
|
||||
),
|
||||
// TODO: test the call of …
|
||||
// sendContributionRejectedEmail: jest.fn((a) => originalModule.sendContributionRejectedEmail(a)),
|
||||
}
|
||||
})
|
||||
|
||||
@ -66,7 +68,7 @@ let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
testEnv = await testEnvironment(logger, localization)
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
@ -366,6 +368,19 @@ describe('AdminResolver', () => {
|
||||
expect(new Date(result.data.deleteUser)).toEqual(expect.any(Date))
|
||||
})
|
||||
|
||||
it('has deleted_at set in users and user contacts', async () => {
|
||||
await expect(
|
||||
User.findOneOrFail({
|
||||
where: { id: user.id },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact'],
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
deletedAt: expect.any(Date),
|
||||
emailContact: expect.objectContaining({ deletedAt: expect.any(Date) }),
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete deleted user', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
@ -489,6 +504,15 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('has deleted_at set to null in users and user contacts', async () => {
|
||||
await expect(
|
||||
User.findOneOrFail({ where: { id: user.id }, relations: ['emailContact'] }),
|
||||
).resolves.toMatchObject({
|
||||
deletedAt: null,
|
||||
emailContact: expect.objectContaining({ deletedAt: null }),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1715,17 +1739,16 @@ describe('AdminResolver', () => {
|
||||
})
|
||||
|
||||
it('calls sendContributionConfirmedEmail', async () => {
|
||||
expect(sendContributionConfirmedEmail).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||
overviewURL: 'http://localhost/overview',
|
||||
recipientEmail: 'bibi@bloxberg.de',
|
||||
recipientFirstName: 'Bibi',
|
||||
recipientLastName: 'Bloxberg',
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
}),
|
||||
)
|
||||
expect(sendContributionConfirmedEmail).toBeCalledWith({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
language: 'de',
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||
contributionAmount: expect.decimalEqual(450),
|
||||
})
|
||||
})
|
||||
|
||||
it('stores the send confirmation email event in the database', async () => {
|
||||
|
||||
@ -39,8 +39,14 @@ import { Decay } from '@model/Decay'
|
||||
import Paginated from '@arg/Paginated'
|
||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||
import { Order } from '@enum/Order'
|
||||
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { getTimeDurationObject } from '@/util/time'
|
||||
import { findUserByEmail, activationLink } from './UserResolver'
|
||||
import {
|
||||
sendAddedContributionMessageEmail,
|
||||
sendAccountActivationEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionRejectedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import CONFIG from '@/config'
|
||||
import {
|
||||
@ -63,9 +69,6 @@ import { ContributionMessage as DbContributionMessage } from '@entity/Contributi
|
||||
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
|
||||
import { ContributionMessageType } from '@enum/MessageType'
|
||||
import { ContributionMessage } from '@model/ContributionMessage'
|
||||
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
|
||||
import { sendContributionRejectedEmail } from '@/mailer/sendContributionRejectedEmail'
|
||||
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import {
|
||||
Event,
|
||||
@ -200,7 +203,7 @@ export class AdminResolver {
|
||||
@Arg('userId', () => Int) userId: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<Date | null> {
|
||||
const user = await dbUser.findOne({ id: userId })
|
||||
const user = await dbUser.findOne({ where: { id: userId }, relations: ['emailContact'] })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
logger.error(`Could not find user with userId: ${userId}`)
|
||||
@ -214,6 +217,7 @@ export class AdminResolver {
|
||||
}
|
||||
// soft-delete user
|
||||
await user.softRemove()
|
||||
await user.emailContact.softRemove()
|
||||
const newUser = await dbUser.findOne({ id: userId }, { withDeleted: true })
|
||||
return newUser ? newUser.deletedAt : null
|
||||
}
|
||||
@ -221,7 +225,10 @@ export class AdminResolver {
|
||||
@Authorized([RIGHTS.UNDELETE_USER])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> {
|
||||
const user = await dbUser.findOne({ id: userId }, { withDeleted: true })
|
||||
const user = await dbUser.findOne(
|
||||
{ id: userId },
|
||||
{ withDeleted: true, relations: ['emailContact'] },
|
||||
)
|
||||
if (!user) {
|
||||
logger.error(`Could not find user with userId: ${userId}`)
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
@ -231,6 +238,7 @@ export class AdminResolver {
|
||||
throw new Error('User is not deleted')
|
||||
}
|
||||
await user.recover()
|
||||
await user.emailContact.recover()
|
||||
return null
|
||||
}
|
||||
|
||||
@ -487,14 +495,13 @@ export class AdminResolver {
|
||||
event.setEventAdminContributionDelete(eventAdminContributionDelete),
|
||||
)
|
||||
sendContributionRejectedEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.emailContact.email,
|
||||
language: user.language,
|
||||
senderFirstName: moderator.firstName,
|
||||
senderLastName: moderator.lastName,
|
||||
recipientEmail: user.emailContact.email,
|
||||
recipientFirstName: user.firstName,
|
||||
recipientLastName: user.lastName,
|
||||
contributionMemo: contribution.memo,
|
||||
contributionAmount: contribution.amount,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
})
|
||||
|
||||
return !!res
|
||||
@ -582,14 +589,14 @@ export class AdminResolver {
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation commited successfuly.')
|
||||
sendContributionConfirmedEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.emailContact.email,
|
||||
language: user.language,
|
||||
senderFirstName: moderatorUser.firstName,
|
||||
senderLastName: moderatorUser.lastName,
|
||||
recipientFirstName: user.firstName,
|
||||
recipientLastName: user.lastName,
|
||||
recipientEmail: user.emailContact.email,
|
||||
contributionMemo: contribution.memo,
|
||||
contributionAmount: contribution.amount,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
})
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
@ -650,17 +657,21 @@ export class AdminResolver {
|
||||
}
|
||||
const emailContact = user.emailContact
|
||||
if (emailContact.deletedAt) {
|
||||
logger.error(`The emailContact: ${email} of htis User is deleted.`)
|
||||
throw new Error(`The emailContact: ${email} of htis User is deleted.`)
|
||||
logger.error(`The emailContact: ${email} of this User is deleted.`)
|
||||
throw new Error(`The emailContact: ${email} of this User is deleted.`)
|
||||
}
|
||||
|
||||
emailContact.emailResendCount++
|
||||
await emailContact.save()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountActivationEmail({
|
||||
link: activationLink(emailContact.emailVerificationCode),
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
language: user.language,
|
||||
activationLink: activationLink(emailContact.emailVerificationCode),
|
||||
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
})
|
||||
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
@ -895,15 +906,13 @@ export class AdminResolver {
|
||||
}
|
||||
|
||||
await sendAddedContributionMessageEmail({
|
||||
firstName: contribution.user.firstName,
|
||||
lastName: contribution.user.lastName,
|
||||
email: contribution.user.emailContact.email,
|
||||
language: contribution.user.language,
|
||||
senderFirstName: user.firstName,
|
||||
senderLastName: user.lastName,
|
||||
recipientFirstName: contribution.user.firstName,
|
||||
recipientLastName: contribution.user.lastName,
|
||||
recipientEmail: contribution.user.emailContact.email,
|
||||
senderEmail: user.emailContact.email,
|
||||
contributionMemo: contribution.memo,
|
||||
message,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
})
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (e) {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import {
|
||||
adminCreateContributionMessage,
|
||||
@ -13,12 +14,16 @@ import { listContributionMessages } from '@/seeds/graphql/queries'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
|
||||
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
|
||||
|
||||
jest.mock('@/mailer/sendAddedContributionMessageEmail', () => {
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
return {
|
||||
__esModule: true,
|
||||
sendAddedContributionMessageEmail: jest.fn(),
|
||||
...originalModule,
|
||||
sendAddedContributionMessageEmail: jest.fn((a) =>
|
||||
originalModule.sendAddedContributionMessageEmail(a),
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
@ -27,7 +32,7 @@ let testEnv: any
|
||||
let result: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
testEnv = await testEnvironment(logger, localization)
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
@ -162,15 +167,13 @@ describe('ContributionMessageResolver', () => {
|
||||
|
||||
it('calls sendAddedContributionMessageEmail', async () => {
|
||||
expect(sendAddedContributionMessageEmail).toBeCalledWith({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
language: 'de',
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
recipientFirstName: 'Bibi',
|
||||
recipientLastName: 'Bloxberg',
|
||||
recipientEmail: 'bibi@bloxberg.de',
|
||||
senderEmail: 'peter@lustig.de',
|
||||
contributionMemo: 'Test env contribution',
|
||||
message: 'Admin Test',
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
126
backend/src/graphql/resolver/EmailOptinCodes.test.ts
Normal file
126
backend/src/graphql/resolver/EmailOptinCodes.test.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { testEnvironment, cleanDB } from '@test/helpers'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { createUser, setPassword, forgotPassword } from '@/seeds/graphql/mutations'
|
||||
import { queryOptIn } from '@/seeds/graphql/queries'
|
||||
import CONFIG from '@/config'
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
CONFIG.EMAIL_CODE_VALID_TIME = 1440
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = 10
|
||||
CONFIG.EMAIL = false
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
describe('EmailOptinCodes', () => {
|
||||
let optinCode: string
|
||||
beforeAll(async () => {
|
||||
const variables = {
|
||||
email: 'peter@lustig.de',
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
language: 'de',
|
||||
}
|
||||
const {
|
||||
data: { createUser: user },
|
||||
} = await mutate({ mutation: createUser, variables })
|
||||
const dbObject = await DbUser.findOneOrFail({
|
||||
where: { id: user.id },
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
optinCode = dbObject.emailContact.emailVerificationCode.toString()
|
||||
})
|
||||
|
||||
describe('queryOptIn', () => {
|
||||
it('has a valid optin code', async () => {
|
||||
await expect(
|
||||
query({ query: queryOptIn, variables: { optIn: optinCode } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
queryOptIn: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
describe('run time forward until code must be expired', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers()
|
||||
setTimeout(jest.fn(), CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000)
|
||||
jest.runAllTimers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
query({ query: queryOptIn, variables: { optIn: optinCode } }),
|
||||
).resolves.toMatchObject({
|
||||
data: null,
|
||||
errors: [new GraphQLError('email was sent more than 24 hours ago')],
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow to set password', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: setPassword, variables: { code: optinCode, password: 'Aa12345_' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: null,
|
||||
errors: [new GraphQLError('email was sent more than 24 hours ago')],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('forgotPassword', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: null,
|
||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||
})
|
||||
})
|
||||
|
||||
describe('run time forward until code can be resent', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers()
|
||||
setTimeout(jest.fn(), CONFIG.EMAIL_CODE_REQUEST_TIME * 60 * 1000)
|
||||
jest.runAllTimers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('cann send email again', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
forgotPassword: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -291,7 +291,6 @@ describe('send coins', () => {
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
/*
|
||||
describe('trying to send negative amount', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
@ -305,18 +304,15 @@ describe('send coins', () => {
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)],
|
||||
errors: [new GraphQLError(`Amount to send must be positive`)],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`user hasn't enough GDD or amount is < 0 : balance=null`,
|
||||
)
|
||||
expect(logger.error).toBeCalledWith(`Amount to send must be positive`)
|
||||
})
|
||||
})
|
||||
*/
|
||||
|
||||
describe('good transaction', () => {
|
||||
it('sends the coins', async () => {
|
||||
|
||||
@ -2,14 +2,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||
import { getCustomRepository, getConnection, In } from '@dbTools/typeorm'
|
||||
|
||||
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
||||
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionList } from '@model/TransactionList'
|
||||
|
||||
@ -36,7 +33,10 @@ import Decimal from 'decimal.js-light'
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
||||
import {
|
||||
sendTransactionLinkRedeemedEmail,
|
||||
sendTransactionReceivedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
|
||||
@ -159,29 +159,27 @@ export const executeTransaction = async (
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.debug(`prepare Email for transaction received...`)
|
||||
// send notification email
|
||||
// TODO: translate
|
||||
await sendTransactionReceivedEmail({
|
||||
firstName: recipient.firstName,
|
||||
lastName: recipient.lastName,
|
||||
email: recipient.emailContact.email,
|
||||
language: recipient.language,
|
||||
senderFirstName: sender.firstName,
|
||||
senderLastName: sender.lastName,
|
||||
recipientFirstName: recipient.firstName,
|
||||
recipientLastName: recipient.lastName,
|
||||
email: recipient.emailContact.email,
|
||||
senderEmail: sender.emailContact.email,
|
||||
amount,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
transactionAmount: amount,
|
||||
})
|
||||
if (transactionLink) {
|
||||
await sendTransactionLinkRedeemedEmail({
|
||||
firstName: sender.firstName,
|
||||
lastName: sender.lastName,
|
||||
email: sender.emailContact.email,
|
||||
language: sender.language,
|
||||
senderFirstName: recipient.firstName,
|
||||
senderLastName: recipient.lastName,
|
||||
recipientFirstName: sender.firstName,
|
||||
recipientLastName: sender.lastName,
|
||||
email: sender.emailContact.email,
|
||||
senderEmail: recipient.emailContact.email,
|
||||
amount,
|
||||
memo,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
transactionAmount: amount,
|
||||
transactionMemo: memo,
|
||||
})
|
||||
}
|
||||
logger.info(`finished executeTransaction successfully`)
|
||||
@ -206,7 +204,7 @@ export class TransactionResolver {
|
||||
// find current balance
|
||||
const lastTransaction = await dbTransaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { balanceDate: 'DESC' } },
|
||||
{ order: { balanceDate: 'DESC' }, relations: ['contribution'] },
|
||||
)
|
||||
logger.debug(`lastTransaction=${lastTransaction}`)
|
||||
|
||||
@ -314,28 +312,16 @@ export class TransactionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
|
||||
if (amount.lte(0)) {
|
||||
logger.error(`Amount to send must be positive`)
|
||||
throw new Error('Amount to send must be positive')
|
||||
}
|
||||
|
||||
// TODO this is subject to replay attacks
|
||||
const senderUser = getUser(context)
|
||||
|
||||
// validate recipient user
|
||||
const recipientUser = await findUserByEmail(email)
|
||||
/*
|
||||
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find UserContact with email: ${email}`)
|
||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||
}
|
||||
*/
|
||||
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
||||
|
||||
/* Code inside this if statement is unreachable (useless by so),
|
||||
in findUserByEmail() an error is already thrown if the user is not found
|
||||
*/
|
||||
if (!recipientUser) {
|
||||
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||
throw new Error('unknown recipient')
|
||||
}
|
||||
if (recipientUser.deletedAt) {
|
||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||
throw new Error('The recipient account was deleted')
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
import { printTimeDuration } from '@/util/time'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import {
|
||||
@ -18,18 +20,17 @@ import { verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queri
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { User } from '@entity/User'
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants'
|
||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||
import { printTimeDuration, activationLink } from './UserResolver'
|
||||
import {
|
||||
sendAccountActivationEmail,
|
||||
sendAccountMultiRegistrationEmail,
|
||||
sendResetPasswordEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||
import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
@ -42,24 +43,16 @@ import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
|
||||
|
||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
|
||||
jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendAccountActivationEmail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
return {
|
||||
__esModule: true,
|
||||
sendAccountMultiRegistrationEmail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/mailer/sendResetPasswordEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendResetPasswordEmail: jest.fn(),
|
||||
...originalModule,
|
||||
sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)),
|
||||
sendAccountMultiRegistrationEmail: jest.fn((a) =>
|
||||
originalModule.sendAccountMultiRegistrationEmail(a),
|
||||
),
|
||||
sendResetPasswordEmail: jest.fn((a) => originalModule.sendResetPasswordEmail(a)),
|
||||
}
|
||||
})
|
||||
|
||||
@ -180,11 +173,15 @@ describe('UserResolver', () => {
|
||||
emailVerificationCode,
|
||||
).replace(/{code}/g, '')
|
||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||
link: activationLink,
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
duration: expect.any(String),
|
||||
language: 'de',
|
||||
activationLink,
|
||||
timeDurationObject: expect.objectContaining({
|
||||
hours: expect.any(Number),
|
||||
minutes: expect.any(Number),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
@ -805,12 +802,8 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('user exists in DB', () => {
|
||||
let emailContact: UserContact
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
// await resetEntity(LoginEmailOptIn)
|
||||
emailContact = await UserContact.findOneOrFail(variables)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -819,7 +812,7 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('duration not expired', () => {
|
||||
it('returns true', async () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
@ -845,15 +838,19 @@ describe('UserResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('sends reset password email', () => {
|
||||
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||
link: activationLink(emailContact.emailVerificationCode),
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
duration: expect.any(String),
|
||||
it('sends reset password email', () => {
|
||||
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
language: 'de',
|
||||
resetLink: expect.any(String),
|
||||
timeDurationObject: expect.objectContaining({
|
||||
hours: expect.any(Number),
|
||||
minutes: expect.any(Number),
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import { User } from '@model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { getTimeDurationObject, printTimeDuration } from '@/util/time'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||
import { encode } from '@/auth/JWT'
|
||||
@ -16,9 +17,11 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
||||
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||
import { OptInType } from '@enum/OptInType'
|
||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants'
|
||||
import {
|
||||
sendAccountActivationEmail,
|
||||
sendAccountMultiRegistrationEmail,
|
||||
sendResetPasswordEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
@ -66,91 +69,6 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||
logger.debug(`newEmailContact...successful: ${emailContact}`)
|
||||
return emailContact
|
||||
}
|
||||
/*
|
||||
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||
logger.trace('newEmailOptIn...')
|
||||
const emailOptIn = new LoginEmailOptIn()
|
||||
emailOptIn.verificationCode = random(64)
|
||||
emailOptIn.userId = userId
|
||||
emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
||||
return emailOptIn
|
||||
}
|
||||
*/
|
||||
/*
|
||||
// needed by AdminResolver
|
||||
// checks if given code exists and can be resent
|
||||
// if optIn does not exits, it is created
|
||||
export const checkOptInCode = async (
|
||||
optInCode: LoginEmailOptIn | undefined,
|
||||
user: DbUser,
|
||||
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
|
||||
): Promise<LoginEmailOptIn> => {
|
||||
logger.info(`checkOptInCode... ${optInCode}`)
|
||||
if (optInCode) {
|
||||
if (!canResendOptIn(optInCode)) {
|
||||
logger.error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
throw new Error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
}
|
||||
optInCode.updatedAt = new Date()
|
||||
optInCode.resendCount++
|
||||
} else {
|
||||
logger.trace('create new OptIn for userId=' + user.id)
|
||||
optInCode = newEmailOptIn(user.id)
|
||||
}
|
||||
|
||||
if (user.emailChecked) {
|
||||
optInCode.emailOptInTypeId = optInType
|
||||
}
|
||||
await LoginEmailOptIn.save(optInCode).catch(() => {
|
||||
logger.error('Unable to save optin code= ' + optInCode)
|
||||
throw new Error('Unable to save optin code.')
|
||||
})
|
||||
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
||||
return optInCode
|
||||
}
|
||||
*/
|
||||
export const checkEmailVerificationCode = async (
|
||||
emailContact: DbUserContact,
|
||||
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
|
||||
): Promise<DbUserContact> => {
|
||||
logger.info(`checkEmailVerificationCode... ${emailContact}`)
|
||||
if (emailContact.updatedAt) {
|
||||
if (!canEmailResend(emailContact.updatedAt)) {
|
||||
logger.error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
throw new Error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
}
|
||||
emailContact.updatedAt = new Date()
|
||||
emailContact.emailResendCount++
|
||||
} else {
|
||||
logger.trace('create new EmailVerificationCode for userId=' + emailContact.userId)
|
||||
emailContact.emailChecked = false
|
||||
emailContact.emailVerificationCode = random(64)
|
||||
}
|
||||
emailContact.emailOptInTypeId = optInType
|
||||
await DbUserContact.save(emailContact).catch(() => {
|
||||
logger.error('Unable to save email verification code= ' + emailContact)
|
||||
throw new Error('Unable to save email verification code.')
|
||||
})
|
||||
logger.debug(`checkEmailVerificationCode...successful: ${emailContact}`)
|
||||
return emailContact
|
||||
}
|
||||
|
||||
export const activationLink = (verificationCode: BigInt): string => {
|
||||
logger.debug(`activationLink(${verificationCode})...`)
|
||||
@ -255,6 +173,7 @@ export class UserResolver {
|
||||
@Authorized([RIGHTS.LOGOUT])
|
||||
@Mutation(() => String)
|
||||
async logout(): Promise<boolean> {
|
||||
// TODO: Event still missing here!!
|
||||
// TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token.
|
||||
// Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login)
|
||||
// The functionality is fully client side - the client just needs to delete his token with the current implementation.
|
||||
@ -405,11 +324,12 @@ export class UserResolver {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountActivationEmail({
|
||||
link: activationLink,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
language,
|
||||
activationLink,
|
||||
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
})
|
||||
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
eventSendConfirmEmail.userId = dbUser.id
|
||||
@ -455,31 +375,45 @@ export class UserResolver {
|
||||
return true
|
||||
}
|
||||
|
||||
// can be both types: REGISTER and RESET_PASSWORD
|
||||
// let optInCode = await LoginEmailOptIn.findOne({
|
||||
// userId: user.id,
|
||||
// })
|
||||
// let optInCode = user.emailContact.emailVerificationCode
|
||||
const dbUserContact = await checkEmailVerificationCode(
|
||||
user.emailContact,
|
||||
OptInType.EMAIL_OPT_IN_RESET_PASSWORD,
|
||||
)
|
||||
if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) {
|
||||
logger.error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
throw new Error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
}
|
||||
|
||||
// optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||
logger.info(`optInCode for ${email}=${dbUserContact}`)
|
||||
user.emailContact.updatedAt = new Date()
|
||||
user.emailContact.emailResendCount++
|
||||
user.emailContact.emailVerificationCode = random(64)
|
||||
user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD
|
||||
await user.emailContact.save().catch(() => {
|
||||
logger.error('Unable to save email verification code= ' + user.emailContact)
|
||||
throw new Error('Unable to save email verification code.')
|
||||
})
|
||||
|
||||
logger.info(`optInCode for ${email}=${user.emailContact}`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendResetPasswordEmailMailer({
|
||||
link: activationLink(dbUserContact.emailVerificationCode),
|
||||
const emailSent = await sendResetPasswordEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
language: user.language,
|
||||
resetLink: activationLink(user.emailContact.emailVerificationCode),
|
||||
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
})
|
||||
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`)
|
||||
logger.debug(
|
||||
`Reset password link: ${activationLink(user.emailContact.emailVerificationCode)}`,
|
||||
)
|
||||
}
|
||||
logger.info(`forgotPassword(${email}) successful...`)
|
||||
|
||||
@ -517,7 +451,7 @@ export class UserResolver {
|
||||
})
|
||||
logger.debug('userContact loaded...')
|
||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
|
||||
logger.error(
|
||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||
)
|
||||
@ -599,7 +533,7 @@ export class UserResolver {
|
||||
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
|
||||
logger.debug(`found optInCode=${userContact}`)
|
||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
|
||||
logger.error(
|
||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||
)
|
||||
@ -759,10 +693,7 @@ const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
||||
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||
}
|
||||
*/
|
||||
const isEmailVerificationCodeValid = (updatedAt: Date | null): boolean => {
|
||||
if (updatedAt == null) {
|
||||
return true
|
||||
}
|
||||
const isEmailVerificationCodeValid = (updatedAt: Date): boolean => {
|
||||
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||
}
|
||||
/*
|
||||
@ -773,20 +704,3 @@ const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
||||
const canEmailResend = (updatedAt: Date): boolean => {
|
||||
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||
}
|
||||
|
||||
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
||||
if (time > 60) {
|
||||
return {
|
||||
hours: Math.floor(time / 60),
|
||||
minutes: time % 60,
|
||||
}
|
||||
}
|
||||
return { minutes: time }
|
||||
}
|
||||
|
||||
export const printTimeDuration = (duration: number): string => {
|
||||
const time = getTimeDurationObject(duration)
|
||||
const result = time.minutes > 0 ? `${time.minutes} minutes` : ''
|
||||
if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '')
|
||||
return result
|
||||
}
|
||||
|
||||
@ -170,8 +170,11 @@ describe('util/creation', () => {
|
||||
const targetDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 0, 0)
|
||||
|
||||
beforeAll(() => {
|
||||
const halfMsToRun = (targetDate.getTime() - now.getTime()) / 2
|
||||
jest.useFakeTimers()
|
||||
setTimeout(jest.fn(), targetDate.getTime() - now.getTime())
|
||||
setTimeout(jest.fn(), halfMsToRun)
|
||||
jest.runAllTimers()
|
||||
setTimeout(jest.fn(), halfMsToRun)
|
||||
jest.runAllTimers()
|
||||
})
|
||||
|
||||
@ -225,8 +228,10 @@ describe('util/creation', () => {
|
||||
})
|
||||
|
||||
it('has the clock set correctly', () => {
|
||||
const targetMonth = nextMonthTargetDate.getMonth() + 1
|
||||
const targetMonthString = (targetMonth < 10 ? '0' : '') + String(targetMonth)
|
||||
expect(new Date().toISOString()).toContain(
|
||||
`${nextMonthTargetDate.getFullYear()}-${nextMonthTargetDate.getMonth() + 1}-01T01:`,
|
||||
`${nextMonthTargetDate.getFullYear()}-${targetMonthString}-01T01:`,
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -103,6 +103,9 @@ export const getUserCreation = async (
|
||||
const getCreationMonths = (timezoneOffset: number): number[] => {
|
||||
const clientNow = new Date()
|
||||
clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000)
|
||||
logger.info(
|
||||
`getCreationMonths -- offset: ${timezoneOffset} -- clientNow: ${clientNow.toISOString()}`,
|
||||
)
|
||||
return [
|
||||
new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1).getMonth() + 1,
|
||||
new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1).getMonth() + 1,
|
||||
|
||||
@ -1,15 +1,61 @@
|
||||
{
|
||||
"emails": {
|
||||
"addedContributionMessage": {
|
||||
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
|
||||
"subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag",
|
||||
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
|
||||
},
|
||||
"accountActivation": {
|
||||
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:",
|
||||
"emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.",
|
||||
"pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:",
|
||||
"subject": "Gradido: E-Mail Überprüfung"
|
||||
},
|
||||
"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}",
|
||||
"emailReused": "deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.",
|
||||
"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",
|
||||
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
|
||||
},
|
||||
"contributionConfirmed": {
|
||||
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.",
|
||||
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"
|
||||
},
|
||||
"contributionRejected": {
|
||||
"commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.",
|
||||
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt",
|
||||
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
|
||||
},
|
||||
"general": {
|
||||
"amountGDD": "Betrag: {amountGDD} GDD",
|
||||
"detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:",
|
||||
"doNotAnswer": "nicht antworten",
|
||||
"helloName": "Hallo {firstName} {lastName},",
|
||||
"linkToYourAccount": "Link zu deinem Konto:",
|
||||
"orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
|
||||
"pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!",
|
||||
"sincerelyYours": "Liebe Grüße",
|
||||
"yourGradidoTeam": "dein Gradido-Team"
|
||||
},
|
||||
"resetPassword": {
|
||||
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:",
|
||||
"pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:",
|
||||
"subject": "Gradido: Passwort zurücksetzen",
|
||||
"youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert."
|
||||
},
|
||||
"transactionLinkRedeemed": {
|
||||
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.",
|
||||
"memo": "Memo: {transactionMemo}",
|
||||
"subject": "Gradido: Dein Gradido-Link wurde eingelöst"
|
||||
},
|
||||
"transactionReceived": {
|
||||
"haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.",
|
||||
"subject": "Gradido: Du hast Gradidos erhalten"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"decimalSeparator": ","
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,61 @@
|
||||
{
|
||||
"emails": {
|
||||
"accountMultiRegistration": {
|
||||
"emails": {
|
||||
"addedContributionMessage": {
|
||||
"commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
|
||||
"subject": "Gradido: Message about your common good contribution",
|
||||
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
|
||||
},
|
||||
"accountActivation": {
|
||||
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:",
|
||||
"emailRegistered": "Your email address has just been registered with Gradido.",
|
||||
"pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:",
|
||||
"subject": "Gradido: Email Verification"
|
||||
},
|
||||
"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",
|
||||
"subject": "Gradido: Try To Register Again With Your Email"
|
||||
},
|
||||
"contributionConfirmed": {
|
||||
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
|
||||
"subject": "Gradido: Your common good contribution was confirmed"
|
||||
},
|
||||
"contributionRejected": {
|
||||
"commonGoodContributionRejected": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
|
||||
"subject": "Gradido: Your common good contribution was rejected",
|
||||
"toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
|
||||
},
|
||||
"general": {
|
||||
"amountGDD": "Amount: {amountGDD} GDD",
|
||||
"detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:",
|
||||
"doNotAnswer": "do not answer",
|
||||
"helloName": "Hello {firstName} {lastName}",
|
||||
"linkToYourAccount": "Link to your account:",
|
||||
"orCopyLink": "or copy the link above into your browser window.",
|
||||
"pleaseDoNotReply": "Please do not reply to this email!",
|
||||
"sincerelyYours": "Kind regards,",
|
||||
"yourGradidoTeam": "your Gradido team"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resetPassword": {
|
||||
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:",
|
||||
"pleaseClickLink": "If it was you, please click on the link:",
|
||||
"subject": "Gradido: Reset password",
|
||||
"youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account."
|
||||
},
|
||||
"transactionLinkRedeemed": {
|
||||
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.",
|
||||
"memo": "Memo: {transactionMemo}",
|
||||
"subject": "Gradido: Your Gradido link has been redeemed"
|
||||
},
|
||||
"transactionReceived": {
|
||||
"haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).",
|
||||
"subject": "Gradido: You have received Gradidos"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"decimalSeparator": "."
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { sendAccountActivationEmail } from './sendAccountActivationEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendAccountActivationEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendAccountActivationEmail({
|
||||
link: 'activationLink',
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
duration: '23 hours and 30 minutes',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Peter Lustig <peter@lustig.de>`,
|
||||
subject: 'Gradido: E-Mail Überprüfung',
|
||||
text:
|
||||
expect.stringContaining('Hallo Peter Lustig') &&
|
||||
expect.stringContaining('activationLink') &&
|
||||
expect.stringContaining('23 Stunden und 30 Minuten'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,17 +0,0 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { accountActivation } from './text/accountActivation'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export const sendAccountActivationEmail = (data: {
|
||||
link: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
duration: string
|
||||
}): Promise<boolean> => {
|
||||
return sendEMail({
|
||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||
subject: accountActivation.de.subject,
|
||||
text: accountActivation.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }),
|
||||
})
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import { sendAddedContributionMessageEmail } from './sendAddedContributionMessageEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendAddedContributionMessageEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendAddedContributionMessageEmail({
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
recipientFirstName: 'Bibi',
|
||||
recipientLastName: 'Bloxberg',
|
||||
recipientEmail: 'bibi@bloxberg.de',
|
||||
senderEmail: 'peter@lustig.de',
|
||||
contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||
message: 'Was für ein Besen ist es geworden?',
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
||||
subject: 'Nachricht zu deinem Gemeinwohl-Beitrag',
|
||||
text:
|
||||
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||
expect.stringContaining('Peter Lustig') &&
|
||||
expect.stringContaining(
|
||||
'du hast zu deinem Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" eine Nachricht von Peter Lustig erhalten.',
|
||||
) &&
|
||||
expect.stringContaining('Was für ein Besen ist es geworden?') &&
|
||||
expect.stringContaining('http://localhost/overview'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,26 +0,0 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { contributionMessageReceived } from './text/contributionMessageReceived'
|
||||
|
||||
export const sendAddedContributionMessageEmail = (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
recipientEmail: string
|
||||
senderEmail: string
|
||||
contributionMemo: string
|
||||
message: string
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>,
|
||||
subject=${contributionMessageReceived.de.subject},
|
||||
text=${contributionMessageReceived.de.text(data)}`,
|
||||
)
|
||||
return sendEMail({
|
||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`,
|
||||
subject: contributionMessageReceived.de.subject,
|
||||
text: contributionMessageReceived.de.text(data),
|
||||
})
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendContributionConfirmedEmail } from './sendContributionConfirmedEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendContributionConfirmedEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendContributionConfirmedEmail({
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
recipientFirstName: 'Bibi',
|
||||
recipientLastName: 'Bloxberg',
|
||||
recipientEmail: 'bibi@bloxberg.de',
|
||||
contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||
contributionAmount: new Decimal(200.0),
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: 'Bibi Bloxberg <bibi@bloxberg.de>',
|
||||
subject: 'Dein Gemeinwohl-Beitrag wurde bestätigt',
|
||||
text:
|
||||
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||
expect.stringContaining(
|
||||
'dein Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde soeben von Peter Lustig bestätigt und in deinem Gradido-Konto gutgeschrieben.',
|
||||
) &&
|
||||
expect.stringContaining('Betrag: 200,00 GDD') &&
|
||||
expect.stringContaining('Link zu deinem Konto: http://localhost/overview'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,26 +0,0 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { contributionConfirmed } from './text/contributionConfirmed'
|
||||
|
||||
export const sendContributionConfirmedEmail = (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
recipientEmail: string
|
||||
contributionMemo: string
|
||||
contributionAmount: Decimal
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>,
|
||||
subject=${contributionConfirmed.de.subject},
|
||||
text=${contributionConfirmed.de.text(data)}`,
|
||||
)
|
||||
return sendEMail({
|
||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`,
|
||||
subject: contributionConfirmed.de.subject,
|
||||
text: contributionConfirmed.de.text(data),
|
||||
})
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendContributionRejectedEmail } from './sendContributionRejectedEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendContributionConfirmedEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendContributionRejectedEmail({
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
recipientFirstName: 'Bibi',
|
||||
recipientLastName: 'Bloxberg',
|
||||
recipientEmail: 'bibi@bloxberg.de',
|
||||
contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||
contributionAmount: new Decimal(200.0),
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: 'Bibi Bloxberg <bibi@bloxberg.de>',
|
||||
subject: 'Dein Gemeinwohl-Beitrag wurde abgelehnt',
|
||||
text:
|
||||
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||
expect.stringContaining(
|
||||
'dein Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde von Peter Lustig abgelehnt.',
|
||||
) &&
|
||||
expect.stringContaining('Link zu deinem Konto: http://localhost/overview'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,26 +0,0 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { contributionRejected } from './text/contributionRejected'
|
||||
|
||||
export const sendContributionRejectedEmail = (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
recipientEmail: string
|
||||
contributionMemo: string
|
||||
contributionAmount: Decimal
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>,
|
||||
subject=${contributionRejected.de.subject},
|
||||
text=${contributionRejected.de.text(data)}`,
|
||||
)
|
||||
return sendEMail({
|
||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`,
|
||||
subject: contributionRejected.de.subject,
|
||||
text: contributionRejected.de.text(data),
|
||||
})
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { createTransport } from 'nodemailer'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
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('sendEMail', () => {
|
||||
let result: boolean
|
||||
describe('config email is false', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
})
|
||||
|
||||
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 () => {
|
||||
jest.clearAllMocks()
|
||||
CONFIG.EMAIL = true
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the transporter', () => {
|
||||
expect(createTransport).toBeCalledWith({
|
||||
host: 'EMAIL_SMTP_URL',
|
||||
port: 1234,
|
||||
secure: false,
|
||||
requireTLS: true,
|
||||
auth: {
|
||||
user: 'user',
|
||||
pass: 'pwd',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendMail of transporter', () => {
|
||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns true', () => {
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with email EMAIL_TEST_MODUS true', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
CONFIG.EMAIL = true
|
||||
CONFIG.EMAIL_TEST_MODUS = true
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendMail of transporter with faked to', () => {
|
||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: CONFIG.EMAIL_TEST_RECEIVER,
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,48 +0,0 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { createTransport } from 'nodemailer'
|
||||
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export const sendEMail = async (emailDef: {
|
||||
to: string
|
||||
cc?: string
|
||||
subject: string
|
||||
text: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`send Email: to=${emailDef.to}` +
|
||||
(emailDef.cc ? `, cc=${emailDef.cc}` : '') +
|
||||
`, subject=${emailDef.subject}, text=${emailDef.text}`,
|
||||
)
|
||||
|
||||
if (!CONFIG.EMAIL) {
|
||||
logger.info(`Emails are disabled via config...`)
|
||||
return false
|
||||
}
|
||||
if (CONFIG.EMAIL_TEST_MODUS) {
|
||||
logger.info(
|
||||
`Testmodus=ON: change receiver from ${emailDef.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||
)
|
||||
emailDef.to = CONFIG.EMAIL_TEST_RECEIVER
|
||||
}
|
||||
const transporter = 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,
|
||||
},
|
||||
})
|
||||
const info = await transporter.sendMail({
|
||||
...emailDef,
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
})
|
||||
if (!info.messageId) {
|
||||
logger.error('error sending notification email, but transaction succeed')
|
||||
throw new Error('error sending notification email, but transaction succeed')
|
||||
}
|
||||
logger.info('send Email successfully.')
|
||||
return true
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import { sendResetPasswordEmail } from './sendResetPasswordEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendResetPasswordEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendResetPasswordEmail({
|
||||
link: 'resetLink',
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
duration: '23 hours and 30 minutes',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Peter Lustig <peter@lustig.de>`,
|
||||
subject: 'Gradido: Passwort zurücksetzen',
|
||||
text:
|
||||
expect.stringContaining('Hallo Peter Lustig') &&
|
||||
expect.stringContaining('resetLink') &&
|
||||
expect.stringContaining('23 Stunden und 30 Minuten'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,17 +0,0 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { resetPassword } from './text/resetPassword'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export const sendResetPasswordEmail = (data: {
|
||||
link: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
duration: string
|
||||
}): Promise<boolean> => {
|
||||
return sendEMail({
|
||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||
subject: resetPassword.de.subject,
|
||||
text: resetPassword.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }),
|
||||
})
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendTransactionLinkRedeemedEmail } from './sendTransactionLinkRedeemed'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendTransactionLinkRedeemedEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendTransactionLinkRedeemedEmail({
|
||||
email: 'bibi@bloxberg.de',
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
recipientFirstName: 'Bibi',
|
||||
recipientLastName: 'Bloxberg',
|
||||
senderEmail: 'peter@lustig.de',
|
||||
amount: new Decimal(42.0),
|
||||
memo: 'Vielen Dank dass Du dabei bist',
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
||||
subject: 'Gradido-Link wurde eingelöst',
|
||||
text:
|
||||
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||
expect.stringContaining(
|
||||
'Peter Lustig (peter@lustig.de) hat soeben deinen Link eingelöst.',
|
||||
) &&
|
||||
expect.stringContaining('Betrag: 42,00 GDD,') &&
|
||||
expect.stringContaining('Memo: Vielen Dank dass Du dabei bist') &&
|
||||
expect.stringContaining(
|
||||
'Details zur Transaktion findest du in deinem Gradido-Konto: http://localhost/overview',
|
||||
) &&
|
||||
expect.stringContaining('Bitte antworte nicht auf diese E-Mail!'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,28 +0,0 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { transactionLinkRedeemed } from './text/transactionLinkRedeemed'
|
||||
|
||||
export const sendTransactionLinkRedeemedEmail = (data: {
|
||||
email: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
|
||||
<${data.email}>,
|
||||
subject=${transactionLinkRedeemed.de.subject},
|
||||
text=${transactionLinkRedeemed.de.text(data)}`,
|
||||
)
|
||||
return sendEMail({
|
||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
||||
subject: transactionLinkRedeemed.de.subject,
|
||||
text: transactionLinkRedeemed.de.text(data),
|
||||
})
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendTransactionReceivedEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendTransactionReceivedEmail({
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
recipientFirstName: 'Peter',
|
||||
recipientLastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
amount: new Decimal(42.0),
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Peter Lustig <peter@lustig.de>`,
|
||||
subject: 'Du hast Gradidos erhalten',
|
||||
text:
|
||||
expect.stringContaining('Hallo Peter Lustig') &&
|
||||
expect.stringContaining('42,00 GDD') &&
|
||||
expect.stringContaining('Bibi Bloxberg') &&
|
||||
expect.stringContaining('(bibi@bloxberg.de)') &&
|
||||
expect.stringContaining('http://localhost/overview'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,27 +0,0 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { transactionReceived } from './text/transactionReceived'
|
||||
|
||||
export const sendTransactionReceivedEmail = (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
|
||||
<${data.email}>,
|
||||
subject=${transactionReceived.de.subject},
|
||||
text=${transactionReceived.de.text(data)}`,
|
||||
)
|
||||
return sendEMail({
|
||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
||||
subject: transactionReceived.de.subject,
|
||||
text: transactionReceived.de.text(data),
|
||||
})
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
export const accountActivation = {
|
||||
de: {
|
||||
subject: 'Gradido: E-Mail Überprüfung',
|
||||
text: (data: {
|
||||
link: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
duration: string
|
||||
resendLink: string
|
||||
}): string =>
|
||||
`Hallo ${data.firstName} ${data.lastName},
|
||||
|
||||
Deine E-Mail-Adresse wurde soeben bei Gradido registriert.
|
||||
|
||||
Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:
|
||||
${data.link}
|
||||
oder kopiere den obigen Link in dein Browserfenster.
|
||||
|
||||
Der Link hat eine Gültigkeit von ${data.duration
|
||||
.replace('hours', 'Stunden')
|
||||
.replace('minutes', 'Minuten')
|
||||
.replace(
|
||||
' and ',
|
||||
' und ',
|
||||
)}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:
|
||||
${data.resendLink}
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
export const accountMultiRegistration = {
|
||||
de: {
|
||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
||||
text: (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
resendLink: string
|
||||
}): string =>
|
||||
`Hallo ${data.firstName} ${data.lastName},
|
||||
|
||||
Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.
|
||||
Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.
|
||||
|
||||
Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:
|
||||
${data.resendLink}
|
||||
oder kopiere den obigen Link in dein Browserfenster.
|
||||
|
||||
Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:
|
||||
https://gradido.net/de/contact/
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const contributionConfirmed = {
|
||||
de: {
|
||||
subject: 'Dein Gemeinwohl-Beitrag wurde bestätigt',
|
||||
text: (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
contributionMemo: string
|
||||
contributionAmount: Decimal
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||
|
||||
dein Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${data.senderFirstName} ${
|
||||
data.senderLastName
|
||||
} bestätigt und in deinem Gradido-Konto gutgeschrieben.
|
||||
|
||||
Betrag: ${data.contributionAmount.toFixed(2).replace('.', ',')} GDD
|
||||
|
||||
Link zu deinem Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Liebe Grüße
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
export const contributionMessageReceived = {
|
||||
de: {
|
||||
subject: 'Nachricht zu deinem Gemeinwohl-Beitrag',
|
||||
text: (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
recipientEmail: string
|
||||
senderEmail: string
|
||||
contributionMemo: string
|
||||
message: string
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||
|
||||
du hast zu deinem Gemeinwohl-Beitrag "${data.contributionMemo}" eine Nachricht von ${data.senderFirstName} ${data.senderLastName} erhalten.
|
||||
|
||||
Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü "Gemeinschaft" auf den Tab "Meine Beiträge zum Gemeinwohl"!
|
||||
|
||||
Link zu deinem Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Liebe Grüße
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const contributionRejected = {
|
||||
de: {
|
||||
subject: 'Dein Gemeinwohl-Beitrag wurde abgelehnt',
|
||||
text: (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
contributionMemo: string
|
||||
contributionAmount: Decimal
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||
|
||||
dein Gemeinwohl-Beitrag "${data.contributionMemo}" wurde von ${data.senderFirstName} ${data.senderLastName} abgelehnt.
|
||||
|
||||
Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü "Gemeinschaft" auf den Tab "Meine Beiträge zum Gemeinwohl"!
|
||||
|
||||
Link zu deinem Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Liebe Grüße
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
export const resetPassword = {
|
||||
de: {
|
||||
subject: 'Gradido: Passwort zurücksetzen',
|
||||
text: (data: {
|
||||
link: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
duration: string
|
||||
resendLink: string
|
||||
}): string =>
|
||||
`Hallo ${data.firstName} ${data.lastName},
|
||||
|
||||
Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert.
|
||||
Wenn du es warst, klicke bitte auf den Link: ${data.link}
|
||||
oder kopiere den obigen Link in Dein Browserfenster.
|
||||
|
||||
Der Link hat eine Gültigkeit von ${data.duration
|
||||
.replace('hours', 'Stunden')
|
||||
.replace('minutes', 'Minuten')
|
||||
.replace(
|
||||
' and ',
|
||||
' und ',
|
||||
)}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:
|
||||
${data.resendLink}
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const transactionLinkRedeemed = {
|
||||
de: {
|
||||
subject: 'Gradido-Link wurde eingelöst',
|
||||
text: (data: {
|
||||
email: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||
|
||||
${data.senderFirstName} ${data.senderLastName} (${
|
||||
data.senderEmail
|
||||
}) hat soeben deinen Link eingelöst.
|
||||
|
||||
Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD,
|
||||
Memo: ${data.memo}
|
||||
|
||||
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Liebe Grüße
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const transactionReceived = {
|
||||
de: {
|
||||
subject: 'Du hast Gradidos erhalten',
|
||||
text: (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||
|
||||
du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
|
||||
data.senderLastName
|
||||
} (${data.senderEmail}) erhalten.
|
||||
|
||||
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Liebe Grüße
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -12,29 +12,24 @@ export class TransactionRepository extends Repository<Transaction> {
|
||||
order: Order,
|
||||
onlyCreation?: boolean,
|
||||
): Promise<[Transaction[], number]> {
|
||||
if (onlyCreation) {
|
||||
return this.createQueryBuilder('userTransaction')
|
||||
.where('userTransaction.userId = :userId', { userId })
|
||||
.andWhere('userTransaction.typeId = :typeId', {
|
||||
typeId: TransactionTypeId.CREATION,
|
||||
})
|
||||
.orderBy('userTransaction.balanceDate', order)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.getManyAndCount()
|
||||
}
|
||||
return this.createQueryBuilder('userTransaction')
|
||||
const query = this.createQueryBuilder('userTransaction')
|
||||
.leftJoinAndSelect(
|
||||
'userTransaction.contribution',
|
||||
'contribution',
|
||||
'userTransaction.id = contribution.transactionId',
|
||||
)
|
||||
.where('userTransaction.userId = :userId', { userId })
|
||||
|
||||
if (onlyCreation) {
|
||||
query.andWhere('userTransaction.typeId = :typeId', {
|
||||
typeId: TransactionTypeId.CREATION,
|
||||
})
|
||||
}
|
||||
|
||||
return query
|
||||
.orderBy('userTransaction.balanceDate', order)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.getManyAndCount()
|
||||
}
|
||||
|
||||
findLastForUser(userId: number): Promise<Transaction | undefined> {
|
||||
return this.createQueryBuilder('userTransaction')
|
||||
.where('userTransaction.userId = :userId', { userId })
|
||||
.orderBy('userTransaction.balanceDate', 'DESC')
|
||||
.getOne()
|
||||
}
|
||||
}
|
||||
|
||||
16
backend/src/util/time.ts
Normal file
16
backend/src/util/time.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
||||
if (time > 60) {
|
||||
return {
|
||||
hours: Math.floor(time / 60),
|
||||
minutes: time % 60,
|
||||
}
|
||||
}
|
||||
return { minutes: time }
|
||||
}
|
||||
|
||||
export const printTimeDuration = (duration: number): string => {
|
||||
const time = getTimeDurationObject(duration)
|
||||
const result = time.minutes > 0 ? `${time.minutes} minutes` : ''
|
||||
if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '')
|
||||
return result
|
||||
}
|
||||
@ -1,5 +1,16 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import i18n from 'i18n'
|
||||
|
||||
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => {
|
||||
return Object.keys(obj).map(function (key) {
|
||||
return obj[key]
|
||||
})
|
||||
}
|
||||
|
||||
export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => {
|
||||
const rememberLocaleToRestore = i18n.getLocale()
|
||||
i18n.setLocale(language)
|
||||
const result = a.toFixed(2).replace('.', i18n.__('general.decimalSeparator'))
|
||||
i18n.setLocale(rememberLocaleToRestore)
|
||||
return result
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ const virtualLinkTransaction = (
|
||||
decay: decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR),
|
||||
memo: '',
|
||||
creationDate: null,
|
||||
contribution: null,
|
||||
...defaultModelFunctions,
|
||||
}
|
||||
return new Transaction(linkDbTransaction, user)
|
||||
@ -78,6 +79,7 @@ const virtualDecayTransaction = (
|
||||
decayStart: decay.start,
|
||||
memo: '',
|
||||
creationDate: null,
|
||||
contribution: null,
|
||||
...defaultModelFunctions,
|
||||
}
|
||||
return new Transaction(decayDbTransaction, user)
|
||||
|
||||
@ -5,6 +5,7 @@ import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../src/server/createServer'
|
||||
import { initialize } from '@dbTools/helpers'
|
||||
import { entities } from '@entity/index'
|
||||
import { i18n, logger } from './testSetup'
|
||||
|
||||
export const headerPushMock = jest.fn((t) => {
|
||||
context.token = t.value
|
||||
@ -26,8 +27,8 @@ export const cleanDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const testEnvironment = async (logger?: any, localization?: any) => {
|
||||
const server = await createServer(context, logger, localization)
|
||||
export const testEnvironment = async (testLogger: any = logger, testI18n: any = i18n) => {
|
||||
const server = await createServer(context, testLogger, testI18n)
|
||||
const con = server.con
|
||||
const testClient = createTestClient(server.apollo)
|
||||
const mutate = testClient.mutate
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import CONFIG from '@/config'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { i18n } from '@/server/localization'
|
||||
|
||||
CONFIG.EMAIL = true
|
||||
CONFIG.EMAIL_TEST_MODUS = false
|
||||
|
||||
jest.setTimeout(1000000)
|
||||
|
||||
jest.mock('@/server/logger', () => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { Contribution } from '../Contribution'
|
||||
|
||||
@Entity('transactions')
|
||||
export class Transaction extends BaseEntity {
|
||||
@ -91,4 +92,8 @@ export class Transaction extends BaseEntity {
|
||||
default: null,
|
||||
})
|
||||
transactionLinkId?: number | null
|
||||
|
||||
@OneToOne(() => Contribution, (contribution) => contribution.transaction)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
|
||||
contribution?: Contribution | null
|
||||
}
|
||||
|
||||
@ -8,10 +8,12 @@ import {
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
} from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { User } from '../User'
|
||||
import { ContributionMessage } from '../ContributionMessage'
|
||||
import { Transaction } from '../Transaction'
|
||||
|
||||
@Entity('contributions')
|
||||
export class Contribution extends BaseEntity {
|
||||
@ -92,4 +94,8 @@ export class Contribution extends BaseEntity {
|
||||
@OneToMany(() => ContributionMessage, (message) => message.contribution)
|
||||
@JoinColumn({ name: 'contribution_id' })
|
||||
messages?: ContributionMessage[]
|
||||
|
||||
@OneToOne(() => Transaction, (transaction) => transaction.contribution)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction?: Transaction | null
|
||||
}
|
||||
|
||||
16
database/migrations/0055-consistent_deleted_users.ts
Normal file
16
database/migrations/0055-consistent_deleted_users.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/* MIGRATION TO soft delete user contacts of soft deleted users */
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
UPDATE user_contacts LEFT JOIN users ON users.email_id = user_contacts.id
|
||||
SET user_contacts.deleted_at = users.deleted_at
|
||||
WHERE user_contacts.deleted_at IS NULL
|
||||
AND users.deleted_at IS NOT NULL;`)
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {}
|
||||
@ -108,7 +108,7 @@ services:
|
||||
#env_file:
|
||||
# - ./frontend/.env
|
||||
volumes:
|
||||
# <host_machine_directy>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
||||
# <host_machine_directory>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
||||
- ./logs/backend:/logs/backend
|
||||
|
||||
########################################################
|
||||
|
||||
379
docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md
Normal file
379
docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md
Normal file
@ -0,0 +1,379 @@
|
||||
# Zeitzonen
|
||||
|
||||
Die Gradido-Anwendung läuft im Backend in der Zeitzone UTC und im Frontend in der jeweiligen lokalen Zeitzone, in der der User sich anmeldet. Dadurch kann es zu zeitlichen Diskrepanzen kommen, die innerhalb der Anwendungslogik aufgelöst bzw. entsprechend behandelt werden müssen. In den folgenden Kapiteln werden die verschiedenen zeitlichen Konstellationen dargestellt und für die verschiedenen fachlichen Prozesse die daraus resultierenden Problemlösungen beschrieben.
|
||||
|
||||

|
||||
|
||||
## Beispiel 1
|
||||
|
||||
Ein User meldet sich in einer Zeitzone t0 - 4 an. Das bedeutet der User liegt 4 Stunden gegenüber der Backend-Zeit zurück.
|
||||
|
||||
Konkret hat der User die Zeit 31.08.2022 21:00:00 auf dem Server ist aber die Zeit bei 01.09.2022 01:00:00
|
||||
|
||||
Für die Erstellung einer Contribution hat der User noch folgende Gültigkeitsmonate und Beträge zur Wahl:
|
||||
|
||||
Juni 2022: 500 GDD | Juli 2022: 200 GDD | August 2022: 1000 GDD
|
||||
|
||||
**aber das Backend liefert nur die Beträge, die eigentlich so korrekt wären!!!!!**
|
||||
|
||||
**Juli 2022: 200 GDD | August 2022: 1000 GDD | September 2022: 1000 GDD**
|
||||
|
||||
Er möchte für den Juni 2022 eine Contribution mit 500 GDD erfassen. **Wird ihm der Juni noch als Schöpfungsmonat angezeigt?**
|
||||
|
||||
Falls ja, dann wählt er dabei im FE im Kalender den 30.06.2022. Dann liefert das FE folgende Contribution-Daten an das Backend:
|
||||
|
||||
* Gültigkeitsdatum: 30.06.2022 00:00:00
|
||||
* Memo: text
|
||||
* Betrag: 500 GDD
|
||||
* **Zeitzone: wird eine Zeitzone des User aus dem Context geliefert? Das fehlt: entweder über eine Zeit vom FE zum BE und ermitteln Offset im BE**
|
||||
|
||||
Im Backend wird dieses dann interpretiert und verarbeitet mit:
|
||||
|
||||
* **Belegung des Schöpfungsmonate-Arrays: [ 6, 7, 8] oder [7, 8, 9] da auf dem Server ja schon der 01.09.2022 ist?**
|
||||
* Gültigkeitsdatum: **30.06.2022 00:00:00 oder 01.07.2022 04:00:00 ?**
|
||||
* Memo: text
|
||||
* Betrag 500 GDD
|
||||
* created_at: 01.07.2022 04:00:00
|
||||
|
||||
**Frage: wird die Contribution dem Juni (6) oder dem Juli (7) zugeordnet?**
|
||||
|
||||
1. falls Juni zugeordnet kann die Contribution mit 500 GDD eingelöst werden
|
||||
2. falls Juli zugeordnet muss die Contribution mit 500 GDD abgelehnt werden, da möglicher Schöpfungsbetrag überschritten
|
||||
|
||||
|
||||
## Beispiel 2
|
||||
|
||||
|
||||
Ein User meldet sich in einer Zeitzone t0 + 1 an. Das bedeutet der User liegt 1 Stunde gegenüber der Backend-Zeit voraus.
|
||||
|
||||
Konkret hat der User die Zeit 01.09.2022 00:20:00 auf dem Server ist aber die Zeit bei 31.08.2022 23:20:00
|
||||
|
||||
Für die Erstellung einer Contribution hat der User noch folgende Gültigkeitsmonate und Beträge zur Wahl:
|
||||
|
||||
Juli 2022: 200 GDD | August 2022: 1000 GDD | September 2022: 1000 GDD
|
||||
|
||||
**oder wird ihm**
|
||||
|
||||
**
|
||||
Juni 2022: 500 GDD | Juli 2022: 200 GDD | August 2022: 1000 GDD**
|
||||
|
||||
**angezeigt, da auf dem BE noch der 31.08.2022 ist?**
|
||||
|
||||
Er möchte für den September 2022 eine Contribution mit 500 GDD erfassen und wählt dabei im FE im Kalender den 01.09.2022. Dann liefert das FE folgende Contribution-Daten an das Backend:
|
||||
|
||||
* Gültigkeitsdatum: 01.09.2022 00:00:00 (siehe Logauszüge der Fehleranalyse im Ticket #2179)
|
||||
* Memo: text
|
||||
* Betrag: 500 GDD
|
||||
* **Zeitzone: wird eine Zeitzone des User aus dem Context geliefert?**
|
||||
|
||||
Im Backend wird dieses dann interpretiert und verarbeitet mit:
|
||||
|
||||
* Belegung des Schöpfungsmonate-Arrays: [ 6, 7, 8] **wie kann der User dann aber vorher September 2022 für die Schöpfung auswählen?**
|
||||
* Gültigkeitsdatum: 01.09.2022 00:00:00
|
||||
* Memo: text
|
||||
* Betrag 500 GDD
|
||||
* created_at: 31.08.2022 23:20:00
|
||||
|
||||
Es kommt zu einem **Fehler im Backend**, da im Schöpfungsmonate-Array kein September (9) vorhanden ist, da auf dem Server noch der 31.08.2022 und damit das Array nur die Monate Juni, Juli, August und nicht September beinhaltet.
|
||||
|
||||
|
||||
## Erkenntnisse:
|
||||
|
||||
* die dem User angezeigten Schöpfungsmonate errechnen sich aus der lokalen User-Zeit und nicht aus der Backend-Zeit
|
||||
* das Backend muss somit für Ermittlung der möglichen Schöpfungsmonate und deren noch freien Schöpfungssummen den UserTimeOffset berücksichten
|
||||
* der gewählte Schöpfungsmonat muss 1:1 vom Frontend in das Backend übertragen werden
|
||||
* es darf kein Mapping in die Backend-Zeit erfolgen
|
||||
* sondern es muss der jeweilige UserTimeOffset mitgespeichert werden
|
||||
* die Logik im BE muss den übertragenen bzw. ermittelten Offset der FE-Zeit entsprechend berücksichten und nicht die Backendzeit in der Logik anwenden
|
||||
* im BE darf es kein einfaches now = new Date() geben
|
||||
* im BE muss stattdessen ein userNow = new Date() + UserTimeOffset verwendet werden
|
||||
* ein CreatedAt / UpdatedAt / DeletedAt / ConfirmedAt wird wie bisher in BE-Zeit gespeichert
|
||||
* **NEIN nicht notwendig:** plus in einer jeweils neuen Spalte CreatedOffset / UpdatedOffset / DeletedOffset / ConfirmedOffset der dabei gültige UserTimeOffset
|
||||
* im FE wird immer im Request-Header der aktuelle Zeitpunkt mit Zeitzone geschrieben
|
||||
*
|
||||
|
||||
## Entscheidung
|
||||
|
||||
* in den HTTP-Request-Header wird generell der aktuelle Timestamp des Clients eingetragen, sodass die aktuelle Uhrzeit des Users ohne weitere Signatur-Änderungen in jedem Aufruf am Backend ankommt. Moritz erstellt Ticket
|
||||
* es wird eine Analyse aller Backend-Aufrufe gemacht, die die Auswertung der User-Time und dessen evtl. Timezone-Differenz in der Logik des Backend-Aufrufs benötigt.
|
||||
* diese Backend-Methoden müssen fachlich so überarbeitet werden, dass immer aus dem Timezone-Offset die korrekte fachliche Logik als Ergebnis heraus kommt. In der Datenbank wird aber immer die UTC-Zeit gespeichert.
|
||||
* Es werden keine zusätzlichen Datanbank-Attribute zur Speicherung des User-TimeOffsets benötigt.
|
||||
|
||||
|
||||
## Analyse der Backend-Aufrufe
|
||||
|
||||
Es werden alle Resolver und ihre Methoden sowie im Resolver exportierte Attribute/Methoden untersucht.
|
||||
|
||||
Mit + gekennzeichnet sind diese, die mit dem UserTimeOffset interagieren und überarbeitet werden müssen.
|
||||
|
||||
Mit - gekennzeichnet sind diese, die keiner weiteren Aktion bedürfen.
|
||||
|
||||
|
||||
### AdminResolver
|
||||
|
||||
#### + adminCreateContribution
|
||||
|
||||
Hier wird der User zur übergebenen Email inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
|
||||
|
||||
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
|
||||
|
||||
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und der Initialisierung der Contribution berücksichtigt werden.
|
||||
|
||||
#### - adminCreateContributionMessage
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + adminCreateContributions
|
||||
|
||||
Hier wird eine Liste von übergebenen Contributions über den internen Aufruf von *adminCreateContribution()* verarbeitet. Da dort eine Berücksichtigung des User-TimeOffsets notwendig ist, muss hier die UserTime entsprechen im Context weitergereicht werden.
|
||||
|
||||
#### - adminDeleteContribution
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + adminUpdateContribution
|
||||
|
||||
analog adminCreateContribution() muss hier der User-TimeOffset berücksichtigt werden.
|
||||
|
||||
#### + confirmContribution
|
||||
|
||||
Hier wird intern *getUserCreation()* und *validateContribution()* aufgerufen, daher analog adminCreateContribution()
|
||||
|
||||
#### + createContributionLink
|
||||
|
||||
Hier werden zwar ein *ValidFrom* und ein *ValidTo* Datum übergeben, doch dürften diese keiner Beachtung eines User-TimezoneOffsets unterliegen. Trotzdem bitte noch einmal verifizieren.
|
||||
|
||||
#### - creationTransactionList
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - deleteContributionLink
|
||||
|
||||
Es wird zwar der *deletedAt*-Zeitpunkt als Rückgabewert geliefert, doch m.E. dürft hier keine Berücksichtigung des User-TimezoneOffsets notwendig sein.
|
||||
|
||||
#### - deleteUser
|
||||
|
||||
Es wird zwar der *deletedAt*-Zeitpunkt als Rückgabewert geliefert, doch m.E. dürft hier keine Berücksichtigung des User-TimezoneOffsets notwendig sein.
|
||||
|
||||
#### - listContributionLinks
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + listTransactionLinksAdmin
|
||||
|
||||
Hier wird die BE-Zeit für die Suche nach ValidUntil verwendet. Dies sollte nocheinmal verifiziert werden.
|
||||
|
||||
#### + listUnconfirmedContributions
|
||||
|
||||
Hier wird intern *getUserCreations()* aufgerufen für die Summen der drei Schöpfungsmonate, somit ist der User-TimezoneOffset zu berücksichtigen.
|
||||
|
||||
#### + searchUsers
|
||||
|
||||
Hier wird intern *getUserCreations()* aufgerufen für die Summen der drei Schöpfungsmonate, somit ist der User-TimezoneOffset zu berücksichtigen.
|
||||
|
||||
#### - sendActivationEmail
|
||||
|
||||
analog *UserResolver.checkOptInCode*
|
||||
|
||||
#### - setUserRole
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - unDeleteUser
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + updateContributionLink
|
||||
|
||||
Hier werden zwar ein *ValidFrom* und ein *ValidTo* Datum übergeben, doch dürften diese keiner Beachtung eines User-TimezoneOffsets unterliegen. Trotzdem bitte noch einmal verifizieren.
|
||||
|
||||
|
||||
### BalanceResolver
|
||||
|
||||
#### + balance
|
||||
|
||||
Hier wird der aktuelle Zeitpunkt des BE verwendet, um den Decay und die Summen der Kontostände zu ermitteln. Dies müsste eigentlich von dem User-TimezoneOffset unabhängig sein. Sollte aber noch einmal dahingehend verifiziert werden.
|
||||
|
||||
|
||||
### CommunityResolver
|
||||
|
||||
#### - communities
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - getCommunityInfo
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### ContributionMessageResolver
|
||||
|
||||
#### - createContributionMessage
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listContributionMessages
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### ContributionResolver
|
||||
|
||||
#### + createContribution
|
||||
|
||||
Hier wird der User inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
|
||||
|
||||
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
|
||||
|
||||
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und der Initialisierung der Contribution berücksichtigt werden.
|
||||
|
||||
#### - deleteContribution
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listAllContributions
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listContributions
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + updateContribution
|
||||
|
||||
Hier werden die Contributions des Users inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
|
||||
|
||||
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
|
||||
|
||||
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und dem Update der Contribution berücksichtigt werden.
|
||||
|
||||
### GdtResolver
|
||||
|
||||
#### - existPid
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - gdtBalance
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listGDTEntries
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### KlicktippResolver
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### StatisticsResolver
|
||||
|
||||
#### + communityStatistics
|
||||
|
||||
Hier werden die Daten zum aktuellen BE-Zeitpunkt ermittelt und dem User angezeigt. Aber der User hat ggf. einen anderen TimeOffset. Daher die Frage, ob die Ermittlung der Statistik-Daten mit dem User-TimeOffset stattfinden muss.
|
||||
|
||||
|
||||
### TransactionLinkResolver
|
||||
|
||||
#### - transactionLinkCode
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - transactionLinkExpireDate
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - createTransactionLink
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - deleteTransactionLink
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listTransactionLinks
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - queryTransactionLink
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - redeemTransactionLink
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### TransactionResolver
|
||||
|
||||
#### - executeTransaction
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - sendCoins
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + transactionList
|
||||
|
||||
Hier wird der aktuelle BE-Zeitpunkt verwendet, um die Summen der vorhandenen Transactions bis zu diesem Zeitpunkt zu ermitteln. Nach ersten Einschätzungen dürfte es hier nichts zu tun geben. Aber es sollte noch einmal geprüft werden.
|
||||
|
||||
|
||||
### UserResolver
|
||||
|
||||
#### - activationLink
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - checkOptInCode
|
||||
|
||||
Hier wird der übergebene OptIn-Code geprüft, ob schon wieder eine erneute Email gesendet werden kann. Die Zeiten werden auf reiner BE-Zeit verglichen, von daher gibt es hier nichts zu tun.
|
||||
|
||||
#### - createUser
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - forgotPassword
|
||||
|
||||
In dieser Methode wird am Ende in der Methode *sendResetPasswordEmailMailer()* die Zeit berechnet, wie lange der OptIn-Code im Link gültig ist, default 1440 min oder 24 h.
|
||||
|
||||
Es ist keine User-TimeOffset zu berücksichten, da der OptInCode direkt als Parameter im Aufruf von queryOptIn verwendet und dann dort mit der BE-Time verglichen wird.
|
||||
|
||||
#### - hasElopage
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - login
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - logout
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - queryOptIn
|
||||
|
||||
Hier wird der OptIn-Code aus der *sendResetPasswordEmailMailer()* als Parameter geliefert. Da dessen Gültigkeit zuvor in forgotPassword mit der BE-Zeit gesetzt wurde, benögt man hier keine Berücksichtigung des User-TimeOffsets.
|
||||
|
||||
#### - searchAdminUsers
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - setPassword
|
||||
|
||||
nothing to do, analog *queryOptIn*
|
||||
|
||||
#### - printTimeDuration
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - updateUserInfos
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + verifyLogin
|
||||
|
||||
Hier wird der User inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contribtutions, egal ob bestätigt oder noch offen ermittelt.
|
||||
|
||||
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
|
||||
@ -0,0 +1,217 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="-PxXzgsMUT8aslXVdGG0" name="Seite-1">
|
||||
<mxGraphModel dx="1022" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="" style="endArrow=classic;html=1;strokeWidth=5;fillColor=#60a917;strokeColor=#2D7600;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="160" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="2160" y="320" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1160" y="360" as="sourcePoint"/>
|
||||
<mxPoint x="1160" y="320" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1130" y="370" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Backend<br style="font-size: 18px;">Zeitzone UTC" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="360" width="200" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="Frontend<br style="font-size: 18px;">verschiedene Zeitzonen" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="240" width="240" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1160" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1160" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1080" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1080" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1000" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1000" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="920" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="920" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="840" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="840" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="760" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="760" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="680" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="680" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="600" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="600" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="520" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="520" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="440" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="440" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="360" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="360" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="279.75" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="279.75" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="200" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="200" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="2120" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="2120" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="2040" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="2040" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1960" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1960" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1880" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1880" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1800" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1800" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1720" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1720" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1640" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1640" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1560" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1560" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1480" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1480" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1400" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1400" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1319.75" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1319.75" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1240" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1240" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="32" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1130" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="33" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 2</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="970" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="36" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 4</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="810" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 6</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="650" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="40" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 8</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="490" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="42" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 10</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="330" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="43" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 10</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1930" y="240" width="70" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="44" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 8</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1770" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="45" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 6</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1610" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="46" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 4</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1450" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="47" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 2</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1290" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="89" value="mögliche Zeitzonen-Konstellationen" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=22;fontStyle=1" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="130" width="400" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@ -38,10 +38,12 @@ export default {
|
||||
form: {
|
||||
text: '',
|
||||
},
|
||||
isSubmitting: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.isSubmitting = true
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createContributionMessage,
|
||||
@ -55,9 +57,11 @@ export default {
|
||||
this.$emit('update-state', this.contributionId)
|
||||
this.form.text = ''
|
||||
this.toastSuccess(this.$t('message.reply'))
|
||||
this.isSubmitting = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
this.isSubmitting = false
|
||||
})
|
||||
},
|
||||
onReset() {
|
||||
@ -66,10 +70,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
disabled() {
|
||||
if (this.form.text !== '') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return this.form.text === '' || this.isSubmitting
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -10,21 +10,21 @@
|
||||
</b-col>
|
||||
<b-col cols="7">
|
||||
<div class="gdd-transaction-list-item-name">
|
||||
<div v-if="linkedUser && linkedUser.email">
|
||||
<span v-if="linkedUser && linkedUser.email">
|
||||
<b-link @click.stop="tunnelEmail">
|
||||
{{ itemText }}
|
||||
</b-link>
|
||||
<span v-if="transactionLinkId">
|
||||
{{ $t('via_link') }}
|
||||
<b-icon
|
||||
icon="link45deg"
|
||||
variant="muted"
|
||||
class="m-mb-1"
|
||||
:title="$t('gdd_per_link.redeemed-title')"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>{{ itemText }}</span>
|
||||
<span v-if="linkId">
|
||||
{{ $t('via_link') }}
|
||||
<b-icon
|
||||
icon="link45deg"
|
||||
variant="muted"
|
||||
class="m-mb-1"
|
||||
:title="$t('gdd_per_link.redeemed-title')"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@ -46,7 +46,7 @@ export default {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
transactionLinkId: {
|
||||
linkId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
@ -12,7 +12,12 @@
|
||||
|
||||
<b-col cols="11">
|
||||
<!-- Amount / Name || Text -->
|
||||
<amount-and-name-row :amount="amount" :linkedUser="linkedUser" v-on="$listeners" />
|
||||
<amount-and-name-row
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
v-on="$listeners"
|
||||
:linkId="linkId"
|
||||
/>
|
||||
|
||||
<!-- Nachricht Memo -->
|
||||
<memo-row :memo="memo" />
|
||||
@ -77,6 +82,10 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
linkId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
previousBookedBalance: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
v-on="$listeners"
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
:transactionLinkId="transactionLinkId"
|
||||
:linkId="linkId"
|
||||
/>
|
||||
|
||||
<!-- Nachricht Memo -->
|
||||
@ -82,7 +82,7 @@ export default {
|
||||
typeId: {
|
||||
type: String,
|
||||
},
|
||||
transactionLinkId: {
|
||||
linkId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
v-on="$listeners"
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
:transactionLinkId="transactionLinkId"
|
||||
:linkId="linkId"
|
||||
/>
|
||||
|
||||
<!-- Memo -->
|
||||
@ -83,7 +83,7 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
transactionLinkId: {
|
||||
linkId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
|
||||
@ -45,7 +45,7 @@ export const transactionsQuery = gql`
|
||||
end
|
||||
duration
|
||||
}
|
||||
transactionLinkId
|
||||
linkId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user