mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge remote-tracking branch 'origin/master' into
2420-feature-federation-implement-exchange-of-api-versions-persist-in-table
This commit is contained in:
commit
26dec0184a
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@ -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 }}
|
||||
|
||||
##########################################################################
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@ -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)
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { JwtPayload } from 'jsonwebtoken'
|
||||
|
||||
export interface CustomJwtPayload extends JwtPayload {
|
||||
pubKey: Buffer
|
||||
gradidoID: string
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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>',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
16
backend/src/emails/templates/greatingFormularImprint.pug
Normal file
16
backend/src/emails/templates/greatingFormularImprint.pug
Normal 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
|
||||
1
backend/src/emails/templates/hello.pug
Normal file
1
backend/src/emails/templates/hello.pug
Normal file
@ -0,0 +1 @@
|
||||
p= t('emails.general.helloName', { firstName, lastName })
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1 +1 @@
|
||||
= t('emails.transactionReceived.subject')
|
||||
= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.')
|
||||
}
|
||||
|
||||
@ -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',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
190
backend/src/graphql/resolver/semaphore.test.ts
Normal file
190
backend/src/graphql/resolver/semaphore.test.ts
Normal 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 })
|
||||
})
|
||||
})
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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...')
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
4
backend/src/util/TRANSACTIONS_LOCK.ts
Normal file
4
backend/src/util/TRANSACTIONS_LOCK.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Semaphore } from 'await-semaphore'
|
||||
|
||||
const CONCURRENT_TRANSACTIONS = 1
|
||||
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)
|
||||
@ -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 {
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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"
|
||||
|
||||
112
database/entity/0057-clear_old_password_junk/User.ts
Normal file
112
database/entity/0057-clear_old_password_junk/User.ts
Normal 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[]
|
||||
}
|
||||
57
database/entity/0057-clear_old_password_junk/UserContact.ts
Normal file
57
database/entity/0057-clear_old_password_junk/UserContact.ts
Normal 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
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Community } from './0056-add_communities_table/Community'
|
||||
export { Community } from './0058-add_communities_table/Community'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { User } from './0053-change_password_encryption/User'
|
||||
export { User } from './0057-clear_old_password_junk/User'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { UserContact } from './0053-change_password_encryption/UserContact'
|
||||
export { UserContact } from './0057-clear_old_password_junk/UserContact'
|
||||
|
||||
39
database/migrations/0056-consistent_transactions_table.ts
Normal file
39
database/migrations/0056-consistent_transactions_table.ts
Normal 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>>) {}
|
||||
16
database/migrations/0057-clear_old_password_junk.ts
Normal file
16
database/migrations/0057-clear_old_password_junk.ts
Normal 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;')
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user