Merge remote-tracking branch 'origin/master' into

2420-feature-federation-implement-exchange-of-api-versions-persist-in-table
This commit is contained in:
Claus-Peter Hübner 2022-12-23 00:02:21 +01:00
commit 26dec0184a
62 changed files with 879 additions and 2481 deletions

View File

@ -139,7 +139,6 @@ jobs:
build_test_nginx: build_test_nginx:
name: Docker Build Test - Nginx name: Docker Build Test - Nginx
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_backend, build_test_admin, build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -528,7 +527,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 74 min_coverage: 76
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################

View File

@ -4,8 +4,30 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.16.0](https://github.com/gradido/gradido/compare/1.15.0...1.16.0)
- refactor(backend): cleaning user related old password junk [`#2426`](https://github.com/gradido/gradido/pull/2426)
- fix(database): consistent transaction table [`#2453`](https://github.com/gradido/gradido/pull/2453)
- refactor(backend): dissolve admin resolver [`#2416`](https://github.com/gradido/gradido/pull/2416)
- fix(backend): email verification code never expired [`#2418`](https://github.com/gradido/gradido/pull/2418)
- fix(database): consistent deleted at bewteen users and user contacts [`#2451`](https://github.com/gradido/gradido/pull/2451)
- feat(backend): log client timezone offset [`#2454`](https://github.com/gradido/gradido/pull/2454)
- refactor(backend): refactor more emails to translatables [`#2398`](https://github.com/gradido/gradido/pull/2398)
- fix(backend): delete / undelete email contact as well [`#2444`](https://github.com/gradido/gradido/pull/2444)
- feat(backend): 🍰 Mark creation via link [`#2363`](https://github.com/gradido/gradido/pull/2363)
- fix(backend): run all timers for high values [`#2452`](https://github.com/gradido/gradido/pull/2452)
- fix(backend): critical bug [`#2443`](https://github.com/gradido/gradido/pull/2443)
- fix(other): missing files for docker production build [`#2442`](https://github.com/gradido/gradido/pull/2442)
- fix(frontend): in contribution messages formular a message can be send twice, when clicking the submit button fast [`#2424`](https://github.com/gradido/gradido/pull/2424)
- fix(backend): wrong month for contribution near turn of month [`#2201`](https://github.com/gradido/gradido/pull/2201)
- feat(backend): add federation config properties [`#2374`](https://github.com/gradido/gradido/pull/2374)
- fix(backend): moved all jest & type-definition related packages into the `devDependencies` section [`#2385`](https://github.com/gradido/gradido/pull/2385)
#### [1.15.0](https://github.com/gradido/gradido/compare/1.14.1...1.15.0) #### [1.15.0](https://github.com/gradido/gradido/compare/1.14.1...1.15.0)
> 26 November 2022
- chore(release): v1.15.0 [`#2425`](https://github.com/gradido/gradido/pull/2425)
- fix(database): wrong balance and decay values [`#2423`](https://github.com/gradido/gradido/pull/2423) - fix(database): wrong balance and decay values [`#2423`](https://github.com/gradido/gradido/pull/2423)
- fix(backend): wrong balance after transaction receive [`#2422`](https://github.com/gradido/gradido/pull/2422) - fix(backend): wrong balance after transaction receive [`#2422`](https://github.com/gradido/gradido/pull/2422)
- feat(other): feature gradido roadmap [`#2301`](https://github.com/gradido/gradido/pull/2301) - feat(other): feature gradido roadmap [`#2301`](https://github.com/gradido/gradido/pull/2301)

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido", "description": "Administraion Interface for Gradido",
"main": "index.js", "main": "index.js",
"author": "Moriz Wahl", "author": "Moriz Wahl",
"version": "1.15.0", "version": "1.16.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {

View File

@ -65,7 +65,6 @@ export default {
}, },
}) })
.then((result) => { .then((result) => {
this.toastSuccess(this.$t('user_recovered'))
this.$emit('updateDeletedAt', { this.$emit('updateDeletedAt', {
userId: this.item.userId, userId: this.item.userId,
deletedAt: result.data.unDeleteUser, deletedAt: result.data.unDeleteUser,

View File

@ -1,4 +1,4 @@
CONFIG_VERSION=v13.2022-11-25 CONFIG_VERSION=v14.2022-12-22
# Server # Server
PORT=4000 PORT=4000
@ -30,6 +30,7 @@ COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code} COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code} COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido. COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# Login Server # Login Server
LOGIN_APP_SECRET=21ffbbc616fe LOGIN_APP_SECRET=21ffbbc616fe
@ -66,5 +67,4 @@ EVENT_PROTOCOL_DISABLED=false
# on an hash created from this topic # on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f # FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
# FEDERATION_DHT_TEST_SOCKET=false
# FEDERATION_COMMUNITY_URL=http://localhost:4000/api # FEDERATION_COMMUNITY_URL=http://localhost:4000/api

View File

@ -29,6 +29,7 @@ COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL
COMMUNITY_REDEEM_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL COMMUNITY_REDEEM_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL
# Login Server # Login Server
LOGIN_APP_SECRET=21ffbbc616fe LOGIN_APP_SECRET=21ffbbc616fe

View File

@ -107,9 +107,7 @@ COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
# Copy log4js-config.json to provide log configuration # Copy log4js-config.json to provide log configuration
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json 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 run scripts run/
# COPY --from=build ${DOCKER_WORKDIR}/run ./run # COPY --from=build ${DOCKER_WORKDIR}/run ./run

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-backend", "name": "gradido-backend",
"version": "1.15.0", "version": "1.16.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions", "description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",
@ -20,6 +20,7 @@
"dependencies": { "dependencies": {
"@hyperswarm/dht": "^6.2.0", "@hyperswarm/dht": "^6.2.0",
"apollo-server-express": "^2.25.2", "apollo-server-express": "^2.25.2",
"await-semaphore": "^0.1.3",
"axios": "^0.21.1", "axios": "^0.21.1",
"class-validator": "^0.13.1", "class-validator": "^0.13.1",
"cors": "^2.8.5", "cors": "^2.8.5",

View File

@ -1,5 +1,5 @@
import { JwtPayload } from 'jsonwebtoken' import { JwtPayload } from 'jsonwebtoken'
export interface CustomJwtPayload extends JwtPayload { export interface CustomJwtPayload extends JwtPayload {
pubKey: Buffer gradidoID: string
} }

View File

@ -11,8 +11,8 @@ export const decode = (token: string): CustomJwtPayload | null => {
} }
} }
export const encode = (pubKey: Buffer): string => { export const encode = (gradidoID: string): string => {
const token = jwt.sign({ pubKey }, CONFIG.JWT_SECRET, { const token = jwt.sign({ gradidoID }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN, expiresIn: CONFIG.JWT_EXPIRES_IN,
}) })
return token return token

View File

@ -10,14 +10,14 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0056-add_communities_table', DB_VERSION: '0058-add_communities_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v13.2022-11-25', EXPECTED: 'v14.2022-11-22',
CURRENT: '', CURRENT: '',
}, },
} }
@ -58,6 +58,7 @@ const community = {
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}', process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION: COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com',
} }
const loginServer = { const loginServer = {
@ -119,7 +120,6 @@ if (
const federation = { const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null, FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_DHT_TEST_SOCKET: process.env.FEDERATION_DHT_TEST_SOCKET === 'true' || false,
FEDERATION_COMMUNITY_URL: FEDERATION_COMMUNITY_URL:
process.env.FEDERATION_COMMUNITY_URL === undefined process.env.FEDERATION_COMMUNITY_URL === undefined
? null ? null

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -70,6 +70,8 @@ describe('sendEmailVariants', () => {
senderLastName: 'Bloxberg', senderLastName: 'Bloxberg',
contributionMemo: 'My contribution.', contributionMemo: 'My contribution.',
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
}) })
@ -106,10 +108,14 @@ describe('sendEmailVariants', () => {
'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!', '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( expect(result.originalMessage.html).toContain(
`Link to your account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`, `Link to your account: <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('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team') expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
}) })
}) })
}) })
@ -140,6 +146,8 @@ describe('sendEmailVariants', () => {
activationLink: 'http://localhost/checkEmail/6627633878930542284', activationLink: 'http://localhost/checkEmail/6627633878930542284',
timeDurationObject: { hours: 23, minutes: 30 }, timeDurationObject: { hours: 23, minutes: 30 },
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
}) })
@ -178,12 +186,16 @@ describe('sendEmailVariants', () => {
'or copy the link above into your browser window.', 'or copy the link above into your browser window.',
) )
expect(result.originalMessage.html).toContain( 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:', '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:',
) )
expect(result.originalMessage.html).toContain( expect(result.originalMessage.html).toContain(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`, `<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
) )
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team') expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
}) })
}) })
}) })
@ -210,6 +222,8 @@ describe('sendEmailVariants', () => {
lastName: 'Lustig', lastName: 'Lustig',
locale: 'en', locale: 'en',
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
}) })
@ -255,9 +269,13 @@ describe('sendEmailVariants', () => {
'or copy the link above into your browser window.', 'or copy the link above into your browser window.',
) )
expect(result.originalMessage.html).toContain( expect(result.originalMessage.html).toContain(
'If you are not the one who tried to register again, please contact our support:', 'If you are not the one who tried to register again, please contact our support:<br><a href="mailto:support@supportmail.com">support@supportmail.com</a>',
)
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
) )
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
}) })
}) })
}) })
@ -292,6 +310,8 @@ describe('sendEmailVariants', () => {
contributionMemo: 'My contribution.', contributionMemo: 'My contribution.',
contributionAmount: '23.54', contributionAmount: '23.54',
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
}) })
@ -326,10 +346,14 @@ describe('sendEmailVariants', () => {
) )
expect(result.originalMessage.html).toContain('Amount: 23.54 GDD') expect(result.originalMessage.html).toContain('Amount: 23.54 GDD')
expect(result.originalMessage.html).toContain( expect(result.originalMessage.html).toContain(
`Link to your account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`, `Link to your account: <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('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team') expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
}) })
}) })
}) })
@ -362,6 +386,8 @@ describe('sendEmailVariants', () => {
senderLastName: 'Bloxberg', senderLastName: 'Bloxberg',
contributionMemo: 'My contribution.', contributionMemo: 'My contribution.',
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
}) })
@ -398,10 +424,14 @@ describe('sendEmailVariants', () => {
'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!', '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( expect(result.originalMessage.html).toContain(
`Link to your account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`, `Link to your account: <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('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team') expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
}) })
}) })
}) })
@ -432,6 +462,8 @@ describe('sendEmailVariants', () => {
resetLink: 'http://localhost/reset-password/3762660021544901417', resetLink: 'http://localhost/reset-password/3762660021544901417',
timeDurationObject: { hours: 23, minutes: 30 }, timeDurationObject: { hours: 23, minutes: 30 },
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
}) })
@ -468,12 +500,16 @@ describe('sendEmailVariants', () => {
'or copy the link above into your browser window.', 'or copy the link above into your browser window.',
) )
expect(result.originalMessage.html).toContain( 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:', '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:',
) )
expect(result.originalMessage.html).toContain( expect(result.originalMessage.html).toContain(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`, `<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
) )
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team') expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
}) })
}) })
}) })
@ -510,6 +546,8 @@ describe('sendEmailVariants', () => {
transactionMemo: 'You deserve it! 🙏🏼', transactionMemo: 'You deserve it! 🙏🏼',
transactionAmount: '17.65', transactionAmount: '17.65',
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
}) })
@ -543,12 +581,16 @@ describe('sendEmailVariants', () => {
'Bibi Bloxberg (bibi@bloxberg.de) has just redeemed your link.', 'Bibi Bloxberg (bibi@bloxberg.de) has just redeemed your link.',
) )
expect(result.originalMessage.html).toContain('Amount: 17.65 GDD') expect(result.originalMessage.html).toContain('Amount: 17.65 GDD')
expect(result.originalMessage.html).toContain('Memo: You deserve it! 🙏🏼') expect(result.originalMessage.html).toContain('Message: You deserve it! 🙏🏼')
expect(result.originalMessage.html).toContain( 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>`, `You can find transaction details in your Gradido account: <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('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team') expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
}) })
}) })
}) })
@ -583,6 +625,8 @@ describe('sendEmailVariants', () => {
senderEmail: 'bibi@bloxberg.de', senderEmail: 'bibi@bloxberg.de',
transactionAmount: '37.40', transactionAmount: '37.40',
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
}) })
@ -598,26 +642,32 @@ describe('sendEmailVariants', () => {
to: 'Peter Lustig <peter@lustig.de>', to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>', from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [], attachments: [],
subject: 'Gradido: You have received Gradidos', subject: 'Gradido: Bibi Bloxberg has sent you 37.40 Gradido',
html: expect.any(String), html: expect.any(String),
text: expect.stringContaining('GRADIDO: YOU HAVE RECEIVED GRADIDOS'), text: expect.stringContaining('GRADIDO: BIBI BLOXBERG HAS SENT YOU 37.40 GRADIDO'),
}), }),
}) })
expect(result.originalMessage.html).toContain('<!DOCTYPE html>') expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">') expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain( expect(result.originalMessage.html).toContain(
'<title>Gradido: You have received Gradidos</title>', '<title>Gradido: Bibi Bloxberg has sent you 37.40 Gradido</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Bibi Bloxberg has sent you 37.40 Gradido</h1>',
) )
expect(result.originalMessage.html).toContain('>Gradido: You have received Gradidos</h1>')
expect(result.originalMessage.html).toContain('Hello Peter Lustig') expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain( expect(result.originalMessage.html).toContain(
'You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).', 'You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).',
) )
expect(result.originalMessage.html).toContain( 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>`, `You can find transaction details in your Gradido account: <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('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team') expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
}) })
}) })
}) })

View File

@ -25,6 +25,8 @@ export const sendAddedContributionMessageEmail = (data: {
senderLastName: data.senderLastName, senderLastName: data.senderLastName,
contributionMemo: data.contributionMemo, contributionMemo: data.contributionMemo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
} }
@ -47,6 +49,8 @@ export const sendAccountActivationEmail = (data: {
activationLink: data.activationLink, activationLink: data.activationLink,
timeDurationObject: data.timeDurationObject, timeDurationObject: data.timeDurationObject,
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
} }
@ -65,6 +69,8 @@ export const sendAccountMultiRegistrationEmail = (data: {
lastName: data.lastName, lastName: data.lastName,
locale: data.language, locale: data.language,
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
} }
@ -91,6 +97,8 @@ export const sendContributionConfirmedEmail = (data: {
contributionMemo: data.contributionMemo, contributionMemo: data.contributionMemo,
contributionAmount: decimalSeparatorByLanguage(data.contributionAmount, data.language), contributionAmount: decimalSeparatorByLanguage(data.contributionAmount, data.language),
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
} }
@ -115,6 +123,8 @@ export const sendContributionRejectedEmail = (data: {
senderLastName: data.senderLastName, senderLastName: data.senderLastName,
contributionMemo: data.contributionMemo, contributionMemo: data.contributionMemo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
} }
@ -137,6 +147,8 @@ export const sendResetPasswordEmail = (data: {
resetLink: data.resetLink, resetLink: data.resetLink,
timeDurationObject: data.timeDurationObject, timeDurationObject: data.timeDurationObject,
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
} }
@ -165,6 +177,8 @@ export const sendTransactionLinkRedeemedEmail = (data: {
transactionMemo: data.transactionMemo, transactionMemo: data.transactionMemo,
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language), transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
} }
@ -191,6 +205,8 @@ export const sendTransactionReceivedEmail = (data: {
senderEmail: data.senderEmail, senderEmail: data.senderEmail,
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language), transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
}, },
}) })
} }

View File

@ -5,16 +5,16 @@ html(lang=locale)
body body
h1(style='margin-bottom: 24px;')= t('emails.accountActivation.subject') h1(style='margin-bottom: 24px;')= t('emails.accountActivation.subject')
#container.col #container.col
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) include ../hello.pug
p= t('emails.accountActivation.emailRegistered') p= t('emails.accountActivation.emailRegistered')
p= t('emails.accountActivation.pleaseClickLink') p
= t('emails.accountActivation.pleaseClickLink')
br br
a(href=activationLink) #{activationLink} a(href=activationLink) #{activationLink}
br br
span= t('emails.general.orCopyLink') = t('emails.general.orCopyLink')
p= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) p
= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
br br
a(href=resendLink) #{resendLink} a(href=resendLink) #{resendLink}
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') include ../greatingFormularImprint.pug
br
span= t('emails.general.yourGradidoTeam')

View File

@ -5,18 +5,19 @@ html(lang=locale)
body body
h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject') h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject')
#container.col #container.col
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) include ../hello.pug
p= t('emails.accountMultiRegistration.emailReused') p
= t('emails.accountMultiRegistration.emailReused')
br br
span= t('emails.accountMultiRegistration.emailExists') = t('emails.accountMultiRegistration.emailExists')
p= t('emails.accountMultiRegistration.onForgottenPasswordClickLink') p
= t('emails.accountMultiRegistration.onForgottenPasswordClickLink')
br br
a(href=resendLink) #{resendLink} a(href=resendLink) #{resendLink}
br br
span= t('emails.accountMultiRegistration.onForgottenPasswordCopyLink') = t('emails.accountMultiRegistration.onForgottenPasswordCopyLink')
p= t('emails.accountMultiRegistration.ifYouAreNotTheOne') p
= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
br br
a(href='https://gradido.net/de/contact/') https://gradido.net/de/contact/ a(href='mailto:' + supportEmail)= supportEmail
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') include ../greatingFormularImprint.pug
br
span= t('emails.general.yourGradidoTeam')

View File

@ -5,13 +5,12 @@ html(lang=locale)
body body
h1(style='margin-bottom: 24px;')= t('emails.addedContributionMessage.subject') h1(style='margin-bottom: 24px;')= t('emails.addedContributionMessage.subject')
#container.col #container.col
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) include ../hello.pug
p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo }) p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.addedContributionMessage.toSeeAndAnswerMessage') p= t('emails.addedContributionMessage.toSeeAndAnswerMessage')
p= t('emails.general.linkToYourAccount') p
span= " " = t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL} a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply') p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') include ../greatingFormularImprint.pug
br
span= t('emails.general.yourGradidoTeam')

View File

@ -5,13 +5,12 @@ html(lang=locale)
body body
h1(style='margin-bottom: 24px;')= t('emails.contributionConfirmed.subject') h1(style='margin-bottom: 24px;')= t('emails.contributionConfirmed.subject')
#container.col #container.col
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) include ../hello.pug
p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo }) p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.general.amountGDD', { amountGDD: contributionAmount }) p= t('emails.general.amountGDD', { amountGDD: contributionAmount })
p= t('emails.general.linkToYourAccount') p
span= " " = t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL} a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply') p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') include ../greatingFormularImprint.pug
br
span= t('emails.general.yourGradidoTeam')

View File

@ -5,13 +5,12 @@ html(lang=locale)
body body
h1(style='margin-bottom: 24px;')= t('emails.contributionRejected.subject') h1(style='margin-bottom: 24px;')= t('emails.contributionRejected.subject')
#container.col #container.col
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) include ../hello.pug
p= t('emails.contributionRejected.commonGoodContributionRejected', { senderFirstName, senderLastName, contributionMemo }) p= t('emails.contributionRejected.commonGoodContributionRejected', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.contributionRejected.toSeeContributionsAndMessages') p= t('emails.contributionRejected.toSeeContributionsAndMessages')
p= t('emails.general.linkToYourAccount') p
span= " " = t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL} a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply') p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') include ../greatingFormularImprint.pug
br
span= t('emails.general.yourGradidoTeam')

View File

@ -0,0 +1,16 @@
p(style='margin-top: 24px;')
= t('emails.general.sincerelyYours')
br
= t('emails.general.yourGradidoTeam')
p(style='margin-top: 24px;')= '—————'
p(style='margin-top: 24px;')
if t('general.imprintImageURL').length > 0
div(style='position: relative; left: -22px;')
img(src=t('general.imprintImageURL'), width='200', alt=t('general.imprintImageAlt'))
br
each line in t('general.imprint').split(/\n/)
= line
br
a(href='mailto:' + supportEmail)= supportEmail
br
a(href=communityURL)= communityURL

View File

@ -0,0 +1 @@
p= t('emails.general.helloName', { firstName, lastName })

View File

@ -5,16 +5,16 @@ html(lang=locale)
body body
h1(style='margin-bottom: 24px;')= t('emails.resetPassword.subject') h1(style='margin-bottom: 24px;')= t('emails.resetPassword.subject')
#container.col #container.col
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) include ../hello.pug
p= t('emails.resetPassword.youOrSomeoneResetPassword') p= t('emails.resetPassword.youOrSomeoneResetPassword')
p= t('emails.resetPassword.pleaseClickLink') p
= t('emails.resetPassword.pleaseClickLink')
br br
a(href=resetLink) #{resetLink} a(href=resetLink) #{resetLink}
br br
span= t('emails.general.orCopyLink') = t('emails.general.orCopyLink')
p= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) p
= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
br br
a(href=resendLink) #{resendLink} a(href=resendLink) #{resendLink}
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') include ../greatingFormularImprint.pug
br
span= t('emails.general.yourGradidoTeam')

View File

@ -5,15 +5,15 @@ html(lang=locale)
body body
h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject') h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject')
#container.col #container.col
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) include ../hello.pug
p= t('emails.transactionLinkRedeemed.hasRedeemedYourLink', { senderFirstName, senderLastName, senderEmail }) p= t('emails.transactionLinkRedeemed.hasRedeemedYourLink', { senderFirstName, senderLastName, senderEmail })
p= t('emails.general.amountGDD', { amountGDD: transactionAmount }) p
= t('emails.general.amountGDD', { amountGDD: transactionAmount })
br br
span= t('emails.transactionLinkRedeemed.memo', { transactionMemo }) = t('emails.transactionLinkRedeemed.memo', { transactionMemo })
p= t('emails.general.detailsYouFindOnLinkToYourAccount') p
span= " " = t('emails.general.detailsYouFindOnLinkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL} a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply') p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') include ../greatingFormularImprint.pug
br
span= t('emails.general.yourGradidoTeam')

View File

@ -1,16 +1,15 @@
doctype html doctype html
html(lang=locale) html(lang=locale)
head head
title= t('emails.transactionReceived.subject') title= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
body body
h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject') h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
#container.col #container.col
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) include ../hello.pug
p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail }) p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail })
p= t('emails.general.detailsYouFindOnLinkToYourAccount') p
span= " " = t('emails.general.detailsYouFindOnLinkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL} a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply') p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') include ../greatingFormularImprint.pug
br
span= t('emails.general.yourGradidoTeam')

View File

@ -1 +1 @@
= t('emails.transactionReceived.subject') = t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })

View File

@ -9,7 +9,6 @@ import { Community as DbCommunity } from '@entity/Community'
import { testEnvironment, cleanDB } from '@test/helpers' import { testEnvironment, cleanDB } from '@test/helpers'
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f' CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
CONFIG.FEDERATION_DHT_TEST_SOCKET = false
jest.mock('@hyperswarm/dht') jest.mock('@hyperswarm/dht')

View File

@ -5,9 +5,8 @@ import { AuthChecker } from 'type-graphql'
import { decode, encode } from '@/auth/JWT' import { decode, encode } from '@/auth/JWT'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES' import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { getCustomRepository } from '@dbTools/typeorm'
import { UserRepository } from '@repository/User'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS' import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { User } from '@entity/User'
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => { const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
context.role = ROLE_UNAUTHORIZED // unauthorized user context.role = ROLE_UNAUTHORIZED // unauthorized user
@ -26,14 +25,16 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
if (!decoded) { if (!decoded) {
throw new Error('403.13 - Client certificate revoked') throw new Error('403.13 - Client certificate revoked')
} }
// Set context pubKey // Set context gradidoID
context.pubKey = Buffer.from(decoded.pubKey).toString('hex') context.gradidoID = decoded.gradidoID
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests // TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey // TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
const userRepository = getCustomRepository(UserRepository)
try { try {
const user = await userRepository.findByPubkeyHex(context.pubKey) const user = await User.findOneOrFail({
where: { gradidoID: decoded.gradidoID },
relations: ['emailContact'],
})
context.user = user context.user = user
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
} catch { } catch {
@ -48,7 +49,7 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
} }
// set new header token // set new header token
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) }) context.setHeaders.push({ key: 'token', value: encode(decoded.gradidoID) })
return true return true
} }

View File

@ -1961,8 +1961,7 @@ describe('ContributionResolver', () => {
}) })
}) })
// In the futrue this should not throw anymore it('throws no error for the second confirmation', async () => {
it('throws an error for the second confirmation', async () => {
const r1 = mutate({ const r1 = mutate({
mutation: confirmContribution, mutation: confirmContribution,
variables: { variables: {
@ -1982,8 +1981,7 @@ describe('ContributionResolver', () => {
) )
await expect(r2).resolves.toEqual( await expect(r2).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
// data: { confirmContribution: true }, data: { confirmContribution: true },
errors: [new GraphQLError('Creation was not successful.')],
}), }),
) )
}) })

View File

@ -50,6 +50,7 @@ import {
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
sendContributionRejectedEmail, sendContributionRejectedEmail,
} from '@/emails/sendEmailVariants' } from '@/emails/sendEmailVariants'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
@Resolver() @Resolver()
export class ContributionResolver { export class ContributionResolver {
@ -579,8 +580,10 @@ export class ContributionResolver {
clientTimezoneOffset, clientTimezoneOffset,
) )
const receivedCallDate = new Date() // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
const receivedCallDate = new Date()
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED') await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
@ -590,7 +593,7 @@ export class ContributionResolver {
.select('transaction') .select('transaction')
.from(DbTransaction, 'transaction') .from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: contribution.userId }) .where('transaction.userId = :id', { id: contribution.userId })
.orderBy('transaction.balanceDate', 'DESC') .orderBy('transaction.id', 'DESC')
.getOne() .getOne()
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined') logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
@ -639,10 +642,11 @@ export class ContributionResolver {
}) })
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Creation was not successful: ${e}`) logger.error('Creation was not successful', e)
throw new Error(`Creation was not successful.`) throw new Error('Creation was not successful.')
} finally { } finally {
await queryRunner.release() await queryRunner.release()
releaseLock()
} }
const event = new Event() const event = new Event()

View File

@ -23,6 +23,11 @@ import { User } from '@entity/User'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
// mock semaphore to allow use fake timers
jest.mock('@/util/TRANSACTIONS_LOCK')
TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
let mutate: any, query: any, con: any let mutate: any, query: any, con: any
let testEnv: any let testEnv: any
@ -185,8 +190,7 @@ describe('TransactionLinkResolver', () => {
describe('after one day', () => { describe('after one day', () => {
beforeAll(async () => { beforeAll(async () => {
jest.useFakeTimers() jest.useFakeTimers()
/* eslint-disable-next-line @typescript-eslint/no-empty-function */ setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
setTimeout(() => {}, 1000 * 60 * 60 * 24)
jest.runAllTimers() jest.runAllTimers()
await mutate({ await mutate({
mutation: login, mutation: login,

View File

@ -31,6 +31,7 @@ import { calculateDecay } from '@/util/decay'
import { getUserCreation, validateContribution } from './util/creations' import { getUserCreation, validateContribution } from './util/creations'
import { executeTransaction } from './TransactionResolver' import { executeTransaction } from './TransactionResolver'
import QueryLinkResult from '@union/QueryLinkResult' import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
// TODO: do not export, test it inside the resolver // TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => { export const transactionLinkCode = (date: Date): string => {
@ -165,10 +166,12 @@ export class TransactionLinkResolver {
): Promise<boolean> { ): Promise<boolean> {
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
const user = getUser(context) const user = getUser(context)
const now = new Date()
if (code.match(/^CL-/)) { if (code.match(/^CL-/)) {
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
logger.info('redeem contribution link...') logger.info('redeem contribution link...')
const now = new Date()
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') await queryRunner.startTransaction('REPEATABLE READ')
@ -273,7 +276,7 @@ export class TransactionLinkResolver {
.select('transaction') .select('transaction')
.from(DbTransaction, 'transaction') .from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: user.id }) .where('transaction.userId = :id', { id: user.id })
.orderBy('transaction.balanceDate', 'DESC') .orderBy('transaction.id', 'DESC')
.getOne() .getOne()
let newBalance = new Decimal(0) let newBalance = new Decimal(0)
@ -309,9 +312,11 @@ export class TransactionLinkResolver {
throw new Error(`Creation from contribution link was not successful. ${e}`) throw new Error(`Creation from contribution link was not successful. ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
releaseLock()
} }
return true return true
} else { } else {
const now = new Date()
const transactionLink = await DbTransactionLink.findOneOrFail({ code }) const transactionLink = await DbTransactionLink.findOneOrFail({ code })
const linkedUser = await DbUser.findOneOrFail( const linkedUser = await DbUser.findOneOrFail(
{ id: transactionLink.userId }, { id: transactionLink.userId },
@ -322,6 +327,9 @@ export class TransactionLinkResolver {
throw new Error('Cannot redeem own transaction link.') throw new Error('Cannot redeem own transaction link.')
} }
// TODO: The now check should be done within the semaphore lock,
// since the program might wait a while till it is ready to proceed
// writing the transaction.
if (transactionLink.validUntil.getTime() < now.getTime()) { if (transactionLink.validUntil.getTime() < now.getTime()) {
throw new Error('Transaction Link is not valid anymore.') throw new Error('Transaction Link is not valid anymore.')
} }

View File

@ -368,5 +368,74 @@ describe('send coins', () => {
) )
}) })
}) })
describe('more transactions to test semaphore', () => {
it('sends the coins four times in a row', async () => {
await expect(
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 10,
memo: 'first transaction',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
},
}),
)
await expect(
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 20,
memo: 'second transaction',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
},
}),
)
await expect(
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 30,
memo: 'third transaction',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
},
}),
)
await expect(
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 40,
memo: 'fourth transaction',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
},
}),
)
})
})
}) })
}) })

View File

@ -16,12 +16,12 @@ import { Transaction } from '@model/Transaction'
import { TransactionList } from '@model/TransactionList' import { TransactionList } from '@model/TransactionList'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
import { TransactionTypeId } from '@enum/TransactionTypeId' import { TransactionTypeId } from '@enum/TransactionTypeId'
import { calculateBalance } from '@/util/validate'
import TransactionSendArgs from '@arg/TransactionSendArgs' import TransactionSendArgs from '@arg/TransactionSendArgs'
import Paginated from '@arg/Paginated' import Paginated from '@arg/Paginated'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { calculateBalance, isHexPublicKey } from '@/util/validate'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { communityUser } from '@/util/communityUser' import { communityUser } from '@/util/communityUser'
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
@ -36,6 +36,8 @@ import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { findUserByEmail } from './UserResolver' import { findUserByEmail } from './UserResolver'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
export const executeTransaction = async ( export const executeTransaction = async (
amount: Decimal, amount: Decimal,
memo: string, memo: string,
@ -62,124 +64,133 @@ export const executeTransaction = async (
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
} }
// validate amount // acquire lock
const receivedCallDate = new Date() const releaseLock = await TRANSACTIONS_LOCK.acquire()
const sendBalance = await calculateBalance(
sender.id,
amount.mul(-1),
receivedCallDate,
transactionLink,
)
logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) {
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
throw new Error("user hasn't enough GDD or amount is < 0")
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
logger.debug(`open Transaction to write...`)
try { try {
// transaction // validate amount
const transactionSend = new dbTransaction() const receivedCallDate = new Date()
transactionSend.typeId = TransactionTypeId.SEND const sendBalance = await calculateBalance(
transactionSend.memo = memo sender.id,
transactionSend.userId = sender.id amount.mul(-1),
transactionSend.linkedUserId = recipient.id receivedCallDate,
transactionSend.amount = amount.mul(-1) transactionLink,
transactionSend.balance = sendBalance.balance )
transactionSend.balanceDate = receivedCallDate logger.debug(`calculated Balance=${sendBalance}`)
transactionSend.decay = sendBalance.decay.decay if (!sendBalance) {
transactionSend.decayStart = sendBalance.decay.start logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
transactionSend.previous = sendBalance.lastTransactionId throw new Error("user hasn't enough GDD or amount is < 0")
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend)
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipient.id
transactionReceive.linkedUserId = sender.id
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
transactionReceive.balanceDate = receivedCallDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = transactionSend.id
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionReceive)
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug(`send Transaction updated: ${transactionSend}`)
if (transactionLink) {
logger.info(`transactionLink: ${transactionLink}`)
transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update(
dbTransactionLink,
{ id: transactionLink.id },
transactionLink,
)
} }
await queryRunner.commitTransaction() const queryRunner = getConnection().createQueryRunner()
logger.info(`commit Transaction successful...`) await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
logger.debug(`open Transaction to write...`)
try {
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = sender.id
transactionSend.linkedUserId = recipient.id
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend)
const eventTransactionSend = new EventTransactionSend() logger.debug(`sendTransaction inserted: ${dbTransaction}`)
eventTransactionSend.userId = transactionSend.userId
eventTransactionSend.xUserId = transactionSend.linkedUserId
eventTransactionSend.transactionId = transactionSend.id
eventTransactionSend.amount = transactionSend.amount.mul(-1)
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
const eventTransactionReceive = new EventTransactionReceive() const transactionReceive = new dbTransaction()
eventTransactionReceive.userId = transactionReceive.userId transactionReceive.typeId = TransactionTypeId.RECEIVE
eventTransactionReceive.xUserId = transactionReceive.linkedUserId transactionReceive.memo = memo
eventTransactionReceive.transactionId = transactionReceive.id transactionReceive.userId = recipient.id
eventTransactionReceive.amount = transactionReceive.amount transactionReceive.linkedUserId = sender.id
await eventProtocol.writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive)) transactionReceive.amount = amount
} catch (e) { const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
await queryRunner.rollbackTransaction() transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
logger.error(`Transaction was not successful: ${e}`) transactionReceive.balanceDate = receivedCallDate
throw new Error(`Transaction was not successful: ${e}`) transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
} finally { transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
await queryRunner.release() transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
} transactionReceive.linkedTransactionId = transactionSend.id
logger.debug(`prepare Email for transaction received...`) transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await sendTransactionReceivedEmail({ await queryRunner.manager.insert(dbTransaction, transactionReceive)
firstName: recipient.firstName, logger.debug(`receive Transaction inserted: ${dbTransaction}`)
lastName: recipient.lastName,
email: recipient.emailContact.email, // Save linked transaction id for send
language: recipient.language, transactionSend.linkedTransactionId = transactionReceive.id
senderFirstName: sender.firstName, await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
senderLastName: sender.lastName, logger.debug(`send Transaction updated: ${transactionSend}`)
senderEmail: sender.emailContact.email,
transactionAmount: amount, if (transactionLink) {
}) logger.info(`transactionLink: ${transactionLink}`)
if (transactionLink) { transactionLink.redeemedAt = receivedCallDate
await sendTransactionLinkRedeemedEmail({ transactionLink.redeemedBy = recipient.id
firstName: sender.firstName, await queryRunner.manager.update(
lastName: sender.lastName, dbTransactionLink,
email: sender.emailContact.email, { id: transactionLink.id },
language: sender.language, transactionLink,
senderFirstName: recipient.firstName, )
senderLastName: recipient.lastName, }
senderEmail: recipient.emailContact.email,
await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
const eventTransactionSend = new EventTransactionSend()
eventTransactionSend.userId = transactionSend.userId
eventTransactionSend.xUserId = transactionSend.linkedUserId
eventTransactionSend.transactionId = transactionSend.id
eventTransactionSend.amount = transactionSend.amount.mul(-1)
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
const eventTransactionReceive = new EventTransactionReceive()
eventTransactionReceive.userId = transactionReceive.userId
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
eventTransactionReceive.transactionId = transactionReceive.id
eventTransactionReceive.amount = transactionReceive.amount
await eventProtocol.writeEvent(
new Event().setEventTransactionReceive(eventTransactionReceive),
)
} catch (e) {
await queryRunner.rollbackTransaction()
logger.error(`Transaction was not successful: ${e}`)
throw new Error(`Transaction was not successful: ${e}`)
} finally {
await queryRunner.release()
}
logger.debug(`prepare Email for transaction received...`)
await sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
email: recipient.emailContact.email,
language: recipient.language,
senderFirstName: sender.firstName,
senderLastName: sender.lastName,
senderEmail: sender.emailContact.email,
transactionAmount: amount, transactionAmount: amount,
transactionMemo: memo,
}) })
if (transactionLink) {
await sendTransactionLinkRedeemedEmail({
firstName: sender.firstName,
lastName: sender.lastName,
email: sender.emailContact.email,
language: sender.language,
senderFirstName: recipient.firstName,
senderLastName: recipient.lastName,
senderEmail: recipient.emailContact.email,
transactionAmount: amount,
transactionMemo: memo,
})
}
logger.info(`finished executeTransaction successfully`)
return true
} finally {
releaseLock()
} }
logger.info(`finished executeTransaction successfully`)
return true
} }
@Resolver() @Resolver()
@ -315,10 +326,6 @@ export class TransactionResolver {
// TODO this is subject to replay attacks // TODO this is subject to replay attacks
const senderUser = getUser(context) const senderUser = getUser(context)
if (senderUser.pubKey.length !== 32) {
logger.error(`invalid sender public key:${senderUser.pubKey}`)
throw new Error('invalid sender public key')
}
// validate recipient user // validate recipient user
const recipientUser = await findUserByEmail(email) const recipientUser = await findUserByEmail(email)
@ -331,10 +338,6 @@ export class TransactionResolver {
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`) logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
throw new Error('The recipient account is not activated') throw new Error('The recipient account is not activated')
} }
if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) {
logger.error(`invalid recipient public key: recipientUser=${recipientUser}`)
throw new Error('invalid recipient public key')
}
await executeTransaction(amount, memo, senderUser, recipientUser) await executeTransaction(amount, memo, senderUser, recipientUser)
logger.info( logger.info(

View File

@ -138,12 +138,8 @@ describe('UserResolver', () => {
firstName: 'Peter', firstName: 'Peter',
lastName: 'Lustig', lastName: 'Lustig',
password: '0', password: '0',
pubKey: null,
privKey: null,
// emailHash: expect.any(Buffer),
createdAt: expect.any(Date), createdAt: expect.any(Date),
// emailChecked: false, // emailChecked: false,
passphrase: expect.any(String),
language: 'de', language: 'de',
isAdmin: null, isAdmin: null,
deletedAt: null, deletedAt: null,

View File

@ -1,4 +1,3 @@
import fs from 'fs'
import i18n from 'i18n' import i18n from 'i18n'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { import {
@ -60,8 +59,8 @@ import {
EventActivateAccount, EventActivateAccount,
} from '@/event/Event' } from '@/event/Event'
import { getUserCreation, getUserCreations } from './util/creations' import { getUserCreation, getUserCreations } from './util/creations'
import { isValidPassword } from '@/password/EncryptorUtils'
import { FULL_CREATION_AVAILABLE } from './const/const' import { FULL_CREATION_AVAILABLE } from './const/const'
import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
@ -76,79 +75,6 @@ const isLanguage = (language: string): boolean => {
return LANGUAGES.includes(language) return LANGUAGES.includes(language)
} }
const PHRASE_WORD_COUNT = 24
const WORDS = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer13116.txt')
.toString()
.split(',')
const PassphraseGenerate = (): string[] => {
logger.trace('PassphraseGenerate...')
const result = []
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
result.push(WORDS[sodium.randombytes_random() % 2048])
}
return result
}
const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
logger.trace('KeyPairEd25519Create...')
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
logger.error('passphrase empty or to short')
throw new Error('passphrase empty or to short')
}
const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES)
sodium.crypto_hash_sha512_init(state)
// To prevent breaking existing passphrase-hash combinations word indices will be put into 64 Bit Variable to mimic first implementation of algorithms
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
const value = Buffer.alloc(8)
const wordIndex = WORDS.indexOf(passphrase[i])
value.writeBigInt64LE(BigInt(wordIndex))
sodium.crypto_hash_sha512_update(state, value)
}
// trailing space is part of the login_server implementation
const clearPassphrase = passphrase.join(' ') + ' '
sodium.crypto_hash_sha512_update(state, Buffer.from(clearPassphrase))
const outputHashBuffer = Buffer.alloc(sodium.crypto_hash_sha512_BYTES)
sodium.crypto_hash_sha512_final(state, outputHashBuffer)
const pubKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
const privKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES)
sodium.crypto_sign_seed_keypair(
pubKey,
privKey,
outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES),
)
logger.debug(`KeyPair creation ready. pubKey=${pubKey}`)
return [pubKey, privKey]
}
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
logger.trace('SecretKeyCryptographyEncrypt...')
const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce
sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey)
logger.debug(`SecretKeyCryptographyEncrypt...successful: ${encrypted}`)
return encrypted
}
const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => {
logger.trace('SecretKeyCryptographyDecrypt...')
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey)
logger.debug(`SecretKeyCryptographyDecrypt...successful: ${message}`)
return message
}
const newEmailContact = (email: string, userId: number): DbUserContact => { const newEmailContact = (email: string, userId: number): DbUserContact => {
logger.trace(`newEmailContact...`) logger.trace(`newEmailContact...`)
const emailContact = new DbUserContact() const emailContact = new DbUserContact()
@ -191,7 +117,6 @@ export class UserResolver {
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
const userEntity = getUser(context) const userEntity = getUser(context)
const user = new User(userEntity, await getUserCreation(userEntity.id, clientTimezoneOffset)) const user = new User(userEntity, await getUserCreation(userEntity.id, clientTimezoneOffset))
// user.pubkey = userEntity.pubKey.toString('hex')
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context) user.hasElopage = await this.hasElopage(context)
@ -223,11 +148,6 @@ export class UserResolver {
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code // TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no password set yet') throw new Error('User has no password set yet')
} }
if (!dbUser.pubKey || !dbUser.privKey) {
logger.error('The User has no private or publicKey.')
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no private or publicKey')
}
if (!verifyPassword(dbUser, password)) { if (!verifyPassword(dbUser, password)) {
logger.error('The User has no valid credentials.') logger.error('The User has no valid credentials.')
@ -259,7 +179,7 @@ export class UserResolver {
context.setHeaders.push({ context.setHeaders.push({
key: 'token', key: 'token',
value: encode(dbUser.pubKey), value: encode(dbUser.gradidoID),
}) })
const ev = new EventLogin() const ev = new EventLogin()
ev.userId = user.id ev.userId = user.id
@ -352,11 +272,6 @@ export class UserResolver {
} }
} }
const passphrase = PassphraseGenerate()
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
// const emailHash = getEmailHash(email)
const gradidoID = await newGradidoID() const gradidoID = await newGradidoID()
const eventRegister = new EventRegister() const eventRegister = new EventRegister()
@ -370,7 +285,6 @@ export class UserResolver {
dbUser.language = language dbUser.language = language
dbUser.publisherId = publisherId dbUser.publisherId = publisherId
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
dbUser.passphrase = passphrase.join(' ')
logger.debug('new dbUser=' + dbUser) logger.debug('new dbUser=' + dbUser)
if (redeemCode) { if (redeemCode) {
if (redeemCode.match(/^CL-/)) { if (redeemCode.match(/^CL-/)) {
@ -391,12 +305,6 @@ export class UserResolver {
} }
} }
} }
// TODO this field has no null allowed unlike the loginServer table
// dbUser.pubKey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
// dbUser.pubkey = keyPair[0]
// loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
// loginUser.pubKey = keyPair[0]
// loginUser.privKey = encryptedPrivkey
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
@ -575,34 +483,12 @@ export class UserResolver {
const user = userContact.user const user = userContact.user
logger.debug('user with EmailVerificationCode found...') logger.debug('user with EmailVerificationCode found...')
// Generate Passphrase if needed
if (!user.passphrase) {
const passphrase = PassphraseGenerate()
user.passphrase = passphrase.join(' ')
logger.debug('new Passphrase generated...')
}
const passphrase = user.passphrase.split(' ')
if (passphrase.length < PHRASE_WORD_COUNT) {
logger.error('Could not load a correct passphrase')
// TODO if this can happen we cannot recover from that
// this seem to be good on production data, if we dont
// make a coding mistake we do not have a problem here
throw new Error('Could not load a correct passphrase')
}
logger.debug('Passphrase is valid...')
// Activate EMail // Activate EMail
userContact.emailChecked = true userContact.emailChecked = true
// Update Password // Update Password
user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
user.password = encryptPassword(user, password) user.password = encryptPassword(user, password)
user.pubKey = keyPair[0]
user.privKey = encryptedPrivkey
logger.debug('User credentials updated ...') logger.debug('User credentials updated ...')
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
@ -713,30 +599,14 @@ export class UserResolver {
) )
} }
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
const oldPasswordHash = SecretKeyCryptographyCreateKey(
userEntity.emailContact.email,
password,
)
if (!verifyPassword(userEntity, password)) { if (!verifyPassword(userEntity, password)) {
logger.error(`Old password is invalid`) logger.error(`Old password is invalid`)
throw new Error(`Old password is invalid`) throw new Error(`Old password is invalid`)
} }
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
logger.debug('oldPassword decrypted...')
const newPasswordHash = SecretKeyCryptographyCreateKey(
userEntity.emailContact.email,
passwordNew,
) // return short and long hash
logger.debug('newPasswordHash created...')
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
logger.debug('PrivateKey encrypted...')
// Save new password hash and newly encrypted private key // Save new password hash and newly encrypted private key
userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
userEntity.password = encryptPassword(userEntity, passwordNew) userEntity.password = encryptPassword(userEntity, passwordNew)
userEntity.privKey = encryptedPrivkey
} }
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()

View File

@ -0,0 +1,190 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Decimal from 'decimal.js-light'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { logger } from '@test/testSetup'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { creationFactory, nMonthsBefore } from '@/seeds/factory/creation'
import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers'
import {
confirmContribution,
createContribution,
createTransactionLink,
redeemTransactionLink,
login,
createContributionLink,
sendCoins,
} from '@/seeds/graphql/mutations'
let mutate: any, con: any
let testEnv: any
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('semaphore', () => {
let contributionLinkCode = ''
let bobsTransactionLinkCode = ''
let bibisTransactionLinkCode = ''
let bibisOpenContributionId = -1
let bobsOpenContributionId = -1
beforeAll(async () => {
const now = new Date()
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bobBaumeister)
await creationFactory(testEnv, {
email: 'bibi@bloxberg.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: nMonthsBefore(new Date()),
confirmed: true,
})
await creationFactory(testEnv, {
email: 'bob@baumeister.de',
amount: 1000,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: nMonthsBefore(new Date()),
confirmed: true,
})
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(200),
name: 'Test Contribution Link',
memo: 'Danke für deine Teilnahme an dem Test der Contribution Links',
cycle: 'ONCE',
validFrom: new Date(2022, 5, 18).toISOString(),
validTo: new Date(now.getFullYear() + 1, 7, 14).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
contributionLinkCode = 'CL-' + contributionLink.code
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bobsLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
email: 'bob@baumeister.de',
amount: 20,
memo: 'Bobs Link',
},
})
const {
data: { createContribution: bobsContribution },
} = await mutate({
mutation: createContribution,
variables: {
creationDate: contributionDateFormatter(new Date()),
amount: 200,
memo: 'Bobs Contribution',
},
})
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bibisLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
amount: 20,
memo: 'Bibis Link',
},
})
const {
data: { createContribution: bibisContribution },
} = await mutate({
mutation: createContribution,
variables: {
creationDate: contributionDateFormatter(new Date()),
amount: 200,
memo: 'Bibis Contribution',
},
})
bobsTransactionLinkCode = bobsLink.code
bibisTransactionLinkCode = bibisLink.code
bibisOpenContributionId = bibisContribution.id
bobsOpenContributionId = bobsContribution.id
})
it('creates a lot of transactions without errors', async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const bibiRedeemContributionLink = mutate({
mutation: redeemTransactionLink,
variables: { code: contributionLinkCode },
})
const redeemBobsLink = mutate({
mutation: redeemTransactionLink,
variables: { code: bobsTransactionLinkCode },
})
const bibisTransaction = mutate({
mutation: sendCoins,
variables: { email: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
})
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
const bobRedeemContributionLink = mutate({
mutation: redeemTransactionLink,
variables: { code: contributionLinkCode },
})
const redeemBibisLink = mutate({
mutation: redeemTransactionLink,
variables: { code: bibisTransactionLinkCode },
})
const bobsTransaction = mutate({
mutation: sendCoins,
variables: { email: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
})
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const confirmBibisContribution = mutate({
mutation: confirmContribution,
variables: { id: bibisOpenContributionId },
})
const confirmBobsContribution = mutate({
mutation: confirmContribution,
variables: { id: bobsOpenContributionId },
})
await expect(bibiRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
await expect(redeemBobsLink).resolves.toMatchObject({ errors: undefined })
await expect(bibisTransaction).resolves.toMatchObject({ errors: undefined })
await expect(bobRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
await expect(redeemBibisLink).resolves.toMatchObject({ errors: undefined })
await expect(bobsTransaction).resolves.toMatchObject({ errors: undefined })
await expect(confirmBibisContribution).resolves.toMatchObject({ errors: undefined })
await expect(confirmBobsContribution).resolves.toMatchObject({ errors: undefined })
})
})

View File

@ -6,7 +6,7 @@
"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“!" "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": { "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:", "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:",
"emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", "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:", "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:",
"subject": "Gradido: E-Mail Überprüfung" "subject": "Gradido: E-Mail Überprüfung"
@ -14,7 +14,7 @@
"accountMultiRegistration": { "accountMultiRegistration": {
"emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.", "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.", "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:", "ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der versucht hat sich erneut zu registrieren, wende dich bitte an unseren Support:",
"onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:", "onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:",
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
@ -40,22 +40,25 @@
"yourGradidoTeam": "dein Gradido-Team" "yourGradidoTeam": "dein Gradido-Team"
}, },
"resetPassword": { "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:", "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:",
"pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:", "pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:",
"subject": "Gradido: Passwort zurücksetzen", "subject": "Gradido: Passwort zurücksetzen",
"youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert."
}, },
"transactionLinkRedeemed": { "transactionLinkRedeemed": {
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.", "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.",
"memo": "Memo: {transactionMemo}", "memo": "Nachricht: {transactionMemo}",
"subject": "Gradido: Dein Gradido-Link wurde eingelöst" "subject": "Gradido: Dein Gradido-Link wurde eingelöst"
}, },
"transactionReceived": { "transactionReceived": {
"haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.", "haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.",
"subject": "Gradido: Du hast Gradidos erhalten" "subject": "Gradido: {senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet"
} }
}, },
"general": { "general": {
"decimalSeparator": "," "decimalSeparator": ",",
"imprint": "Gradido-Akademie\nInstitut für Wirtschaftsbionik\nPfarrweg 2\n74653 Künzelsau\nDeutschland",
"imprintImageAlt": "Gradido-Akademie Logo",
"imprintImageURL": "https://gdd.gradido.net/img/brand/green.png"
} }
} }

View File

@ -6,7 +6,7 @@
"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!" "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": { "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:", "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:",
"emailRegistered": "Your email address has just been registered with Gradido.", "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:", "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:",
"subject": "Gradido: Email Verification" "subject": "Gradido: Email Verification"
@ -40,22 +40,25 @@
"yourGradidoTeam": "your Gradido team" "yourGradidoTeam": "your Gradido team"
}, },
"resetPassword": { "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:", "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:",
"pleaseClickLink": "If it was you, please click on the link:", "pleaseClickLink": "If it was you, please click on the link:",
"subject": "Gradido: Reset password", "subject": "Gradido: Reset password",
"youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account."
}, },
"transactionLinkRedeemed": { "transactionLinkRedeemed": {
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.", "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.",
"memo": "Memo: {transactionMemo}", "memo": "Message: {transactionMemo}",
"subject": "Gradido: Your Gradido link has been redeemed" "subject": "Gradido: Your Gradido link has been redeemed"
}, },
"transactionReceived": { "transactionReceived": {
"haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).", "haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).",
"subject": "Gradido: You have received Gradidos" "subject": "Gradido: {senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido"
} }
}, },
"general": { "general": {
"decimalSeparator": "." "decimalSeparator": ".",
"imprint": "Gradido-Akademie\nInstitut für Wirtschaftsbionik\nPfarrweg 2\n74653 Künzelsau\nDeutschland",
"imprintImageAlt": "Gradido-Akademie Logo",
"imprintImageURL": "https://gdd.gradido.net/img/brand/green.png"
} }
} }

View File

@ -75,10 +75,7 @@ const run = async () => {
// create GDD // create GDD
for (let i = 0; i < creations.length; i++) { for (let i = 0; i < creations.length; i++) {
const now = new Date().getTime() // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
await creationFactory(seedClient, creations[i]) await creationFactory(seedClient, creations[i])
// eslint-disable-next-line no-empty
while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
} }
logger.info('##seed## seeding all creations successful...') logger.info('##seed## seeding all creations successful...')

View File

@ -4,21 +4,6 @@ import { User as DbUser } from '@entity/User'
@EntityRepository(DbUser) @EntityRepository(DbUser)
export class UserRepository extends Repository<DbUser> { export class UserRepository extends Repository<DbUser> {
async findByPubkeyHex(pubkeyHex: string): Promise<DbUser> {
const dbUser = await this.createQueryBuilder('user')
.leftJoinAndSelect('user.emailContact', 'emailContact')
.where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
.getOneOrFail()
/*
const dbUser = await this.findOneOrFail(`hex(user.pubKey) = { pubkeyHex }`)
const emailContact = await this.query(
`SELECT * from user_contacts where id = { dbUser.emailId }`,
)
dbUser.emailContact = emailContact
*/
return dbUser
}
async findBySearchCriteriaPagedFiltered( async findBySearchCriteriaPagedFiltered(
select: string[], select: string[],
searchCriteria: string, searchCriteria: string,

View File

@ -0,0 +1,4 @@
import { Semaphore } from 'await-semaphore'
const CONCURRENT_TRANSACTIONS = 1
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)

View File

@ -16,8 +16,6 @@ const communityDbUser: dbUser = {
emailId: -1, emailId: -1,
firstName: 'Gradido', firstName: 'Gradido',
lastName: 'Akademie', lastName: 'Akademie',
pubKey: Buffer.from(''),
privKey: Buffer.from(''),
deletedAt: null, deletedAt: null,
password: BigInt(0), password: BigInt(0),
// emailHash: Buffer.from(''), // emailHash: Buffer.from(''),
@ -26,7 +24,6 @@ const communityDbUser: dbUser = {
language: '', language: '',
isAdmin: null, isAdmin: null,
publisherId: 0, publisherId: 0,
passphrase: '',
// default password encryption type // default password encryption type
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
hasId: function (): boolean { hasId: function (): boolean {

View File

@ -14,17 +14,13 @@ function isStringBoolean(value: string): boolean {
return false return false
} }
function isHexPublicKey(publicKey: string): boolean {
return /^[0-9A-Fa-f]{64}$/i.test(publicKey)
}
async function calculateBalance( async function calculateBalance(
userId: number, userId: number,
amount: Decimal, amount: Decimal,
time: Date, time: Date,
transactionLink?: dbTransactionLink | null, transactionLink?: dbTransactionLink | null,
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { ): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } }) const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } })
if (!lastTransaction) return null if (!lastTransaction) return null
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
@ -45,4 +41,4 @@ async function calculateBalance(
return { balance, lastTransactionId: lastTransaction.id, decay } return { balance, lastTransactionId: lastTransaction.id, decay }
} }
export { isHexPublicKey, calculateBalance, isStringBoolean } export { calculateBalance, isStringBoolean }

View File

@ -1643,6 +1643,11 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
await-semaphore@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3"
integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==
axios@^0.21.1: axios@^0.21.1:
version "0.21.4" version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"

View File

@ -0,0 +1,112 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToMany,
JoinColumn,
OneToOne,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { ContributionMessage } from '../ContributionMessage'
import { UserContact } from '../UserContact'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({
name: 'gradido_id',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
gradidoID: string
@Column({
name: 'alias',
length: 20,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
alias: string
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
@JoinColumn({ name: 'email_id' })
emailContact: UserContact
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
emailId: number | null
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
deletedAt: Date | null
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({
name: 'password_encryption_type',
type: 'int',
unsigned: true,
nullable: false,
default: 0,
})
passwordEncryptionType: number
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
isAdmin: Date | null
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
referrerId?: number | null
@Column({
name: 'contribution_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
contributionLinkId?: number | null
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
@OneToMany(() => ContributionMessage, (message) => message.user)
@JoinColumn({ name: 'user_id' })
messages?: ContributionMessage[]
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
@JoinColumn({ name: 'user_id' })
userContacts?: UserContact[]
}

View File

@ -0,0 +1,57 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToOne,
} from 'typeorm'
import { User } from './User'
@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class UserContact extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({
name: 'type',
length: 100,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
type: string
@OneToOne(() => User, (user) => user.emailContact)
user: User
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false })
userId: number
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true })
emailVerificationCode: BigInt
@Column({ name: 'email_opt_in_type_id' })
emailOptInTypeId: number
@Column({ name: 'email_resend_count' })
emailResendCount: number
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
@Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' })
phone: string
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' })
updatedAt: Date | null
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
deletedAt: Date | null
}

View File

@ -1 +1 @@
export { Community } from './0056-add_communities_table/Community' export { Community } from './0058-add_communities_table/Community'

View File

@ -1 +1 @@
export { User } from './0053-change_password_encryption/User' export { User } from './0057-clear_old_password_junk/User'

View File

@ -1 +1 @@
export { UserContact } from './0053-change_password_encryption/UserContact' export { UserContact } from './0057-clear_old_password_junk/UserContact'

View File

@ -0,0 +1,39 @@
/* MIGRATION TO add users that have a transaction but do not exist */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { v4 as uuidv4 } from 'uuid'
import { OkPacket } from 'mysql'
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
const missingUserIds = await queryFn(`
SELECT user_id FROM transactions
WHERE NOT EXISTS (SELECT id FROM users WHERE id = user_id) GROUP BY user_id;`)
for (let i = 0; i < missingUserIds.length; i++) {
let gradidoId = ''
let countIds: any[] = []
do {
gradidoId = uuidv4()
countIds = await queryFn(
`SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`,
)
} while (countIds[0] > 0)
const userContact = (await queryFn(`
INSERT INTO user_contacts
(type, user_id, email, email_checked, created_at, deleted_at)
VALUES
('EMAIL', ${missingUserIds[i].user_id}, 'deleted.user${missingUserIds[i].user_id}@gradido.net', 0, NOW(), NOW());`)) as unknown as OkPacket
await queryFn(`
INSERT INTO users
(id, gradido_id, email_id, first_name, last_name, deleted_at, password_encryption_type, created_at, language)
VALUES
(${missingUserIds[i].user_id}, '${gradidoId}', ${userContact.insertId}, 'DELETED', 'USER', NOW(), 0, NOW(), 'de');`)
}
}
/* 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>>) {}

View File

@ -0,0 +1,16 @@
/* 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('ALTER TABLE users DROP COLUMN public_key;')
await queryFn('ALTER TABLE users DROP COLUMN privkey;')
await queryFn('ALTER TABLE users DROP COLUMN email_hash;')
await queryFn('ALTER TABLE users DROP COLUMN passphrase;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE users ADD COLUMN public_key binary(32) DEFAULT NULL;')
await queryFn('ALTER TABLE users ADD COLUMN privkey binary(80) DEFAULT NULL;')
await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;')
await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;')
}

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-database", "name": "gradido-database",
"version": "1.15.0", "version": "1.16.0",
"description": "Gradido Database Tool to execute database migrations", "description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database", "repository": "https://github.com/gradido/gradido/database",

View File

@ -24,9 +24,10 @@ COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code} COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code} COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# backend # backend
BACKEND_CONFIG_VERSION=v12.2022-11-10 BACKEND_CONFIG_VERSION=v13.2022-12-20
JWT_EXPIRES_IN=10m JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net GDT_API_URL=https://gdt.gradido.net
@ -69,7 +70,7 @@ EVENT_PROTOCOL_DISABLED=false
DATABASE_CONFIG_VERSION=v1.2022-03-18 DATABASE_CONFIG_VERSION=v1.2022-03-18
# frontend # frontend
FRONTEND_CONFIG_VERSION=v3.2022-09-16 FRONTEND_CONFIG_VERSION=v4.2022-12-20
GRAPHQL_URI=https://stage1.gradido.net/graphql GRAPHQL_URI=https://stage1.gradido.net/graphql
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token} ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
@ -85,8 +86,6 @@ META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natü
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System" META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie" META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
SUPPORT_MAIL=support@supportmail.com
# admin # admin
ADMIN_CONFIG_VERSION=v1.2022-03-18 ADMIN_CONFIG_VERSION=v1.2022-03-18

View File

@ -1,4 +1,4 @@
CONFIG_VERSION=v3.2022-09-16 CONFIG_VERSION=v4.2022-12-20
# Environment # Environment
DEFAULT_PUBLISHER_ID=2896 DEFAULT_PUBLISHER_ID=2896
@ -12,6 +12,7 @@ COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/ COMMUNITY_URL=http://localhost/
COMMUNITY_REGISTER_URL=http://localhost/register COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido. COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# Meta # Meta
META_URL=http://localhost META_URL=http://localhost
@ -22,6 +23,3 @@ META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more peo
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem" META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System" META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie" META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
# Support Mail
SUPPORT_MAIL=support@supportmail.com

View File

@ -12,6 +12,7 @@ COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_URL=$COMMUNITY_URL COMMUNITY_URL=$COMMUNITY_URL
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL
# Meta # Meta
META_URL=$META_URL META_URL=$META_URL
@ -22,6 +23,3 @@ META_DESCRIPTION_EN=$META_DESCRIPTION_EN
META_KEYWORDS_DE=$META_KEYWORDS_DE META_KEYWORDS_DE=$META_KEYWORDS_DE
META_KEYWORDS_EN=$META_KEYWORDS_EN META_KEYWORDS_EN=$META_KEYWORDS_EN
META_AUTHOR=$META_AUTHOR META_AUTHOR=$META_AUTHOR
# Support Mail
SUPPORT_MAIL=$SUPPORT_MAIL

View File

@ -1,6 +1,6 @@
{ {
"name": "bootstrap-vue-gradido-wallet", "name": "bootstrap-vue-gradido-wallet",
"version": "1.15.0", "version": "1.16.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node run/server.js", "start": "node run/server.js",

View File

@ -8,7 +8,7 @@ const constants = {
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v3.2022-09-16', EXPECTED: 'v4.2022-12-20',
CURRENT: '', CURRENT: '',
}, },
} }
@ -39,6 +39,7 @@ const community = {
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register', COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
COMMUNITY_DESCRIPTION: COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com',
} }
const meta = { const meta = {
@ -60,10 +61,6 @@ const meta = {
META_AUTHOR: process.env.META_AUTHOR || 'Bernd Hückstädt - Gradido-Akademie', META_AUTHOR: process.env.META_AUTHOR || 'Bernd Hückstädt - Gradido-Akademie',
} }
const supportmail = {
SUPPORT_MAIL: process.env.SUPPORT_MAIL || 'support@supportmail.com',
}
// Check config version // Check config version
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
if ( if (
@ -83,7 +80,6 @@ const CONFIG = {
...endpoints, ...endpoints,
...community, ...community,
...meta, ...meta,
...supportmail,
} }
module.exports = CONFIG module.exports = CONFIG

View File

@ -89,7 +89,7 @@ export default {
countAdminUser: null, countAdminUser: null,
itemsContributionLinks: [], itemsContributionLinks: [],
itemsAdminUser: [], itemsAdminUser: [],
supportMail: CONFIG.SUPPORT_MAIL, supportMail: CONFIG.COMMUNITY_SUPPORT_MAIL,
membersCount: '1203', membersCount: '1203',
totalUsers: null, totalUsers: null,
totalGradidoCreated: null, totalGradidoCreated: null,

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido", "name": "gradido",
"version": "1.15.0", "version": "1.16.0",
"description": "Gradido", "description": "Gradido",
"main": "index.js", "main": "index.js",
"repository": "git@github.com:gradido/gradido.git", "repository": "git@github.com:gradido/gradido.git",