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:
name: Docker Build Test - Nginx
runs-on: ubuntu-latest
needs: [build_test_backend, build_test_admin, build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -528,7 +527,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 74
min_coverage: 76
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).
#### [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)
> 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(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)

View File

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

View File

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

View File

@ -1,4 +1,4 @@
CONFIG_VERSION=v13.2022-11-25
CONFIG_VERSION=v14.2022-12-22
# Server
PORT=4000
@ -30,6 +30,7 @@ COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# Login Server
LOGIN_APP_SECRET=21ffbbc616fe
@ -66,5 +67,4 @@ EVENT_PROTOCOL_DISABLED=false
# on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
# FEDERATION_DHT_TEST_SOCKET=false
# 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_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL
# Login Server
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 log4js-config.json to provide log configuration
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json
# Copy memonic type since its referenced in the sources
# TODO: remove
COPY --from=build ${DOCKER_WORKDIR}/src/config/mnemonic.uncompressed_buffer13116.txt ./src/config/mnemonic.uncompressed_buffer13116.txt
# Copy run scripts run/
# COPY --from=build ${DOCKER_WORKDIR}/run ./run

View File

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

View File

@ -1,5 +1,5 @@
import { JwtPayload } from 'jsonwebtoken'
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 => {
const token = jwt.sign({ pubKey }, CONFIG.JWT_SECRET, {
export const encode = (gradidoID: string): string => {
const token = jwt.sign({ gradidoID }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN,
})
return token

View File

@ -10,14 +10,14 @@ Decimal.set({
})
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
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v13.2022-11-25',
EXPECTED: 'v14.2022-11-22',
CURRENT: '',
},
}
@ -58,6 +58,7 @@ const community = {
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com',
}
const loginServer = {
@ -119,7 +120,6 @@ if (
const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 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:
process.env.FEDERATION_COMMUNITY_URL === undefined
? 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',
contributionMemo: 'My contribution.',
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!',
)
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('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',
timeDurationObject: { hours: 23, minutes: 30 },
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.',
)
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(
`<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',
locale: 'en',
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.',
)
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.',
contributionAmount: '23.54',
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(
`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('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',
contributionMemo: 'My contribution.',
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!',
)
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('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',
timeDurationObject: { hours: 23, minutes: 30 },
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.',
)
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(
`<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! 🙏🏼',
transactionAmount: '17.65',
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.',
)
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(
`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('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',
transactionAmount: '37.40',
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>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: You have received Gradidos',
subject: 'Gradido: Bibi Bloxberg has sent you 37.40 Gradido',
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('<html lang="en">')
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(
'You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).',
)
expect(result.originalMessage.html).toContain(
`You can find transaction details in your Gradido account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
`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('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,
contributionMemo: data.contributionMemo,
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,
timeDurationObject: data.timeDurationObject,
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,
locale: data.language,
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,
contributionAmount: decimalSeparatorByLanguage(data.contributionAmount, data.language),
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,
contributionMemo: data.contributionMemo,
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,
timeDurationObject: data.timeDurationObject,
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,
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
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,
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
},
})
}

View File

@ -5,16 +5,16 @@ html(lang=locale)
body
h1(style='margin-bottom: 24px;')= t('emails.accountActivation.subject')
#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.pleaseClickLink')
p
= t('emails.accountActivation.pleaseClickLink')
br
a(href=activationLink) #{activationLink}
br
span= t('emails.general.orCopyLink')
p= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
= t('emails.general.orCopyLink')
p
= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
br
a(href=resendLink) #{resendLink}
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
br
span= t('emails.general.yourGradidoTeam')
include ../greatingFormularImprint.pug

View File

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

View File

@ -5,13 +5,12 @@ html(lang=locale)
body
h1(style='margin-bottom: 24px;')= t('emails.addedContributionMessage.subject')
#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.toSeeAndAnswerMessage')
p= t('emails.general.linkToYourAccount')
span= " "
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
br
span= t('emails.general.yourGradidoTeam')
include ../greatingFormularImprint.pug

View File

@ -5,13 +5,12 @@ html(lang=locale)
body
h1(style='margin-bottom: 24px;')= t('emails.contributionConfirmed.subject')
#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.general.amountGDD', { amountGDD: contributionAmount })
p= t('emails.general.linkToYourAccount')
span= " "
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
br
span= t('emails.general.yourGradidoTeam')
include ../greatingFormularImprint.pug

View File

@ -5,13 +5,12 @@ html(lang=locale)
body
h1(style='margin-bottom: 24px;')= t('emails.contributionRejected.subject')
#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.toSeeContributionsAndMessages')
p= t('emails.general.linkToYourAccount')
span= " "
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
br
span= t('emails.general.yourGradidoTeam')
include ../greatingFormularImprint.pug

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
h1(style='margin-bottom: 24px;')= t('emails.resetPassword.subject')
#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.pleaseClickLink')
p
= t('emails.resetPassword.pleaseClickLink')
br
a(href=resetLink) #{resetLink}
br
span= t('emails.general.orCopyLink')
p= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
= t('emails.general.orCopyLink')
p
= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
br
a(href=resendLink) #{resendLink}
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
br
span= t('emails.general.yourGradidoTeam')
include ../greatingFormularImprint.pug

View File

@ -5,15 +5,15 @@ html(lang=locale)
body
h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject')
#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.general.amountGDD', { amountGDD: transactionAmount })
p
= t('emails.general.amountGDD', { amountGDD: transactionAmount })
br
span= t('emails.transactionLinkRedeemed.memo', { transactionMemo })
p= t('emails.general.detailsYouFindOnLinkToYourAccount')
span= " "
= t('emails.transactionLinkRedeemed.memo', { transactionMemo })
p
= t('emails.general.detailsYouFindOnLinkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
br
span= t('emails.general.yourGradidoTeam')
include ../greatingFormularImprint.pug

View File

@ -1,16 +1,15 @@
doctype html
html(lang=locale)
head
title= t('emails.transactionReceived.subject')
title= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
body
h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject')
h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
#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.general.detailsYouFindOnLinkToYourAccount')
span= " "
p
= t('emails.general.detailsYouFindOnLinkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
br
span= t('emails.general.yourGradidoTeam')
include ../greatingFormularImprint.pug

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'
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
CONFIG.FEDERATION_DHT_TEST_SOCKET = false
jest.mock('@hyperswarm/dht')

View File

@ -5,9 +5,8 @@ import { AuthChecker } from 'type-graphql'
import { decode, encode } from '@/auth/JWT'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { RIGHTS } from '@/auth/RIGHTS'
import { getCustomRepository } from '@dbTools/typeorm'
import { UserRepository } from '@repository/User'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { User } from '@entity/User'
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
context.role = ROLE_UNAUTHORIZED // unauthorized user
@ -26,14 +25,16 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
if (!decoded) {
throw new Error('403.13 - Client certificate revoked')
}
// Set context pubKey
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
// Set context gradidoID
context.gradidoID = decoded.gradidoID
// 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
const userRepository = getCustomRepository(UserRepository)
try {
const user = await userRepository.findByPubkeyHex(context.pubKey)
const user = await User.findOneOrFail({
where: { gradidoID: decoded.gradidoID },
relations: ['emailContact'],
})
context.user = user
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
} catch {
@ -48,7 +49,7 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
}
// set new header token
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
context.setHeaders.push({ key: 'token', value: encode(decoded.gradidoID) })
return true
}

View File

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

View File

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

View File

@ -23,6 +23,11 @@ import { User } from '@entity/User'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import Decimal from 'decimal.js-light'
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 testEnv: any
@ -185,8 +190,7 @@ describe('TransactionLinkResolver', () => {
describe('after one day', () => {
beforeAll(async () => {
jest.useFakeTimers()
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
setTimeout(() => {}, 1000 * 60 * 60 * 24)
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
jest.runAllTimers()
await mutate({
mutation: login,

View File

@ -31,6 +31,7 @@ import { calculateDecay } from '@/util/decay'
import { getUserCreation, validateContribution } from './util/creations'
import { executeTransaction } from './TransactionResolver'
import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
// TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => {
@ -165,10 +166,12 @@ export class TransactionLinkResolver {
): Promise<boolean> {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const user = getUser(context)
const now = new Date()
if (code.match(/^CL-/)) {
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
logger.info('redeem contribution link...')
const now = new Date()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
@ -273,7 +276,7 @@ export class TransactionLinkResolver {
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: user.id })
.orderBy('transaction.balanceDate', 'DESC')
.orderBy('transaction.id', 'DESC')
.getOne()
let newBalance = new Decimal(0)
@ -309,9 +312,11 @@ export class TransactionLinkResolver {
throw new Error(`Creation from contribution link was not successful. ${e}`)
} finally {
await queryRunner.release()
releaseLock()
}
return true
} else {
const now = new Date()
const transactionLink = await DbTransactionLink.findOneOrFail({ code })
const linkedUser = await DbUser.findOneOrFail(
{ id: transactionLink.userId },
@ -322,6 +327,9 @@ export class TransactionLinkResolver {
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()) {
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 { Order } from '@enum/Order'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { calculateBalance } from '@/util/validate'
import TransactionSendArgs from '@arg/TransactionSendArgs'
import Paginated from '@arg/Paginated'
import { backendLogger as logger } from '@/server/logger'
import { Context, getUser } from '@/server/context'
import { calculateBalance, isHexPublicKey } from '@/util/validate'
import { RIGHTS } from '@/auth/RIGHTS'
import { communityUser } from '@/util/communityUser'
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 { findUserByEmail } from './UserResolver'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
export const executeTransaction = async (
amount: Decimal,
memo: string,
@ -62,124 +64,133 @@ export const executeTransaction = async (
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
}
// validate amount
const receivedCallDate = new Date()
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")
}
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
const queryRunner = getConnection().createQueryRunner()
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)
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,
)
// validate amount
const receivedCallDate = new Date()
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")
}
await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
const queryRunner = getConnection().createQueryRunner()
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()
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))
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
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,
})
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,
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()
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,
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()
@ -315,10 +326,6 @@ export class TransactionResolver {
// TODO this is subject to replay attacks
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
const recipientUser = await findUserByEmail(email)
@ -331,10 +338,6 @@ export class TransactionResolver {
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
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)
logger.info(

View File

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

View File

@ -1,4 +1,3 @@
import fs from 'fs'
import i18n from 'i18n'
import { v4 as uuidv4 } from 'uuid'
import {
@ -60,8 +59,8 @@ import {
EventActivateAccount,
} from '@/event/Event'
import { getUserCreation, getUserCreations } from './util/creations'
import { isValidPassword } from '@/password/EncryptorUtils'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
@ -76,79 +75,6 @@ const isLanguage = (language: string): boolean => {
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 => {
logger.trace(`newEmailContact...`)
const emailContact = new DbUserContact()
@ -191,7 +117,6 @@ export class UserResolver {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const userEntity = getUser(context)
const user = new User(userEntity, await getUserCreation(userEntity.id, clientTimezoneOffset))
// user.pubkey = userEntity.pubKey.toString('hex')
// Elopage Status & Stored PublisherId
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
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)) {
logger.error('The User has no valid credentials.')
@ -259,7 +179,7 @@ export class UserResolver {
context.setHeaders.push({
key: 'token',
value: encode(dbUser.pubKey),
value: encode(dbUser.gradidoID),
})
const ev = new EventLogin()
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 eventRegister = new EventRegister()
@ -370,7 +285,6 @@ export class UserResolver {
dbUser.language = language
dbUser.publisherId = publisherId
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
dbUser.passphrase = passphrase.join(' ')
logger.debug('new dbUser=' + dbUser)
if (redeemCode) {
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()
await queryRunner.connect()
@ -575,34 +483,12 @@ export class UserResolver {
const user = userContact.user
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
userContact.emailChecked = true
// Update Password
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.pubKey = keyPair[0]
user.privKey = encryptedPrivkey
logger.debug('User credentials updated ...')
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)) {
logger.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
userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
userEntity.password = encryptPassword(userEntity, passwordNew)
userEntity.privKey = encryptedPrivkey
}
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“!"
},
"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.",
"pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:",
"subject": "Gradido: E-Mail Überprüfung"
@ -14,7 +14,7 @@
"accountMultiRegistration": {
"emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.",
"emailReused": "deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.",
"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:",
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
@ -40,22 +40,25 @@
"yourGradidoTeam": "dein Gradido-Team"
},
"resetPassword": {
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:",
"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:",
"subject": "Gradido: Passwort zurücksetzen",
"youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert."
},
"transactionLinkRedeemed": {
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.",
"memo": "Memo: {transactionMemo}",
"memo": "Nachricht: {transactionMemo}",
"subject": "Gradido: Dein Gradido-Link wurde eingelöst"
},
"transactionReceived": {
"haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.",
"subject": "Gradido: Du hast Gradidos erhalten"
"subject": "Gradido: {senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet"
}
},
"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!"
},
"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.",
"pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:",
"subject": "Gradido: Email Verification"
@ -40,22 +40,25 @@
"yourGradidoTeam": "your Gradido team"
},
"resetPassword": {
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:",
"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:",
"subject": "Gradido: Reset password",
"youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account."
},
"transactionLinkRedeemed": {
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.",
"memo": "Memo: {transactionMemo}",
"memo": "Message: {transactionMemo}",
"subject": "Gradido: Your Gradido link has been redeemed"
},
"transactionReceived": {
"haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).",
"subject": "Gradido: You have received Gradidos"
"subject": "Gradido: {senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido"
}
},
"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
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])
// 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...')

View File

@ -4,21 +4,6 @@ import { User as DbUser } from '@entity/User'
@EntityRepository(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(
select: 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,
firstName: 'Gradido',
lastName: 'Akademie',
pubKey: Buffer.from(''),
privKey: Buffer.from(''),
deletedAt: null,
password: BigInt(0),
// emailHash: Buffer.from(''),
@ -26,7 +24,6 @@ const communityDbUser: dbUser = {
language: '',
isAdmin: null,
publisherId: 0,
passphrase: '',
// default password encryption type
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
hasId: function (): boolean {

View File

@ -14,17 +14,13 @@ function isStringBoolean(value: string): boolean {
return false
}
function isHexPublicKey(publicKey: string): boolean {
return /^[0-9A-Fa-f]{64}$/i.test(publicKey)
}
async function calculateBalance(
userId: number,
amount: Decimal,
time: Date,
transactionLink?: dbTransactionLink | 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
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
@ -45,4 +41,4 @@ async function calculateBalance(
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"
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:
version "0.21.4"
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",
"version": "1.15.0",
"version": "1.16.0",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"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_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# backend
BACKEND_CONFIG_VERSION=v12.2022-11-10
BACKEND_CONFIG_VERSION=v13.2022-12-20
JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net
@ -69,7 +70,7 @@ EVENT_PROTOCOL_DISABLED=false
DATABASE_CONFIG_VERSION=v1.2022-03-18
# frontend
FRONTEND_CONFIG_VERSION=v3.2022-09-16
FRONTEND_CONFIG_VERSION=v4.2022-12-20
GRAPHQL_URI=https://stage1.gradido.net/graphql
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_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
SUPPORT_MAIL=support@supportmail.com
# admin
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
DEFAULT_PUBLISHER_ID=2896
@ -12,6 +12,7 @@ COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/
COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# Meta
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_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"
# Support Mail
SUPPORT_MAIL=support@supportmail.com

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
"version": "1.15.0",
"version": "1.16.0",
"private": true,
"scripts": {
"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
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v3.2022-09-16',
EXPECTED: 'v4.2022-12-20',
CURRENT: '',
},
}
@ -39,6 +39,7 @@ const community = {
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com',
}
const meta = {
@ -60,10 +61,6 @@ const meta = {
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
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
if (
@ -83,7 +80,6 @@ const CONFIG = {
...endpoints,
...community,
...meta,
...supportmail,
}
module.exports = CONFIG

View File

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

View File

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