Merge branch 'master' into 2290-New-Design

This commit is contained in:
Alexander Friedland 2022-12-04 11:35:39 +01:00 committed by GitHub
commit f458d305fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 687 additions and 61 deletions

View File

@ -4,8 +4,21 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.15.0](https://github.com/gradido/gradido/compare/1.14.1...1.15.0)
- 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)
- refactor(backend): new password encryption implementation [`#2353`](https://github.com/gradido/gradido/pull/2353)
- refactor(admin): statistics in a table and on separate page in admin area [`#2399`](https://github.com/gradido/gradido/pull/2399)
- feat(backend): 🍰 Email Templates [`#2163`](https://github.com/gradido/gradido/pull/2163)
- fix(backend): timezone problems [`#2393`](https://github.com/gradido/gradido/pull/2393)
#### [1.14.1](https://github.com/gradido/gradido/compare/1.14.0...1.14.1) #### [1.14.1](https://github.com/gradido/gradido/compare/1.14.0...1.14.1)
> 14 November 2022
- chore(release): version 1.14.1 - hotfix [`#2391`](https://github.com/gradido/gradido/pull/2391)
- fix(frontend): load contributionMessages is fixed [`#2390`](https://github.com/gradido/gradido/pull/2390) - fix(frontend): load contributionMessages is fixed [`#2390`](https://github.com/gradido/gradido/pull/2390)
#### [1.14.0](https://github.com/gradido/gradido/compare/1.13.3...1.14.0) #### [1.14.0](https://github.com/gradido/gradido/compare/1.13.3...1.14.0)

View File

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

View File

@ -38,10 +38,12 @@ export default {
form: { form: {
text: '', text: '',
}, },
loading: false,
} }
}, },
methods: { methods: {
onSubmit(event) { onSubmit(event) {
this.loading = true
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: adminCreateContributionMessage, mutation: adminCreateContributionMessage,
@ -55,9 +57,11 @@ export default {
this.$emit('update-state', this.contributionId) this.$emit('update-state', this.contributionId)
this.form.text = '' this.form.text = ''
this.toastSuccess(this.$t('message.request')) this.toastSuccess(this.$t('message.request'))
this.loading = false
}) })
.catch((error) => { .catch((error) => {
this.toastError(error.message) this.toastError(error.message)
this.loading = false
}) })
}, },
onReset(event) { onReset(event) {
@ -66,10 +70,7 @@ export default {
}, },
computed: { computed: {
disabled() { disabled() {
if (this.form.text !== '') { return this.form.text === '' || this.loading
return false
}
return true
}, },
}, },
} }

View File

@ -1,4 +1,4 @@
CONFIG_VERSION=v11.2022-10-27 CONFIG_VERSION=v12.2022-11-10
# Server # Server
PORT=4000 PORT=4000
@ -61,7 +61,8 @@ EVENT_PROTOCOL_DISABLED=false
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info # LOG_LEVEL=info
# DHT # Federation
# if you set this value, the DHT hyperswarm will start to announce and listen # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this tpoic # on an hash created from this topic
# DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f

View File

@ -56,5 +56,6 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
# EventProtocol # EventProtocol
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
# DHT # Federation
DHT_TOPIC=$DHT_TOPIC FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED

View File

@ -98,10 +98,18 @@ COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
# We also copy the node_modules express and serve-static for the run script # We also copy the node_modules express and serve-static for the run script
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules
# Copy static files # Copy static files
# COPY --from=build ${DOCKER_WORKDIR}/public ./public # COPY --from=build ${DOCKER_WORKDIR}/public ./public
# Copy package.json for script definitions (lock file should not be needed) # Copy package.json for script definitions (lock file should not be needed)
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Copy tsconfig.json to provide alias path definitions
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
# Copy log4js-config.json to provide log configuration
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json
# Copy memonic type since its referenced in the sources
# TODO: remove
COPY --from=build ${DOCKER_WORKDIR}/src/config/mnemonic.uncompressed_buffer13116.txt ./src/config/mnemonic.uncompressed_buffer13116.txt
# Copy run scripts run/ # Copy run scripts run/
# COPY --from=build ${DOCKER_WORKDIR}/run ./run # COPY --from=build ${DOCKER_WORKDIR}/run ./run

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-backend", "name": "gradido-backend",
"version": "1.14.1", "version": "1.15.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions", "description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",
@ -19,13 +19,7 @@
}, },
"dependencies": { "dependencies": {
"@hyperswarm/dht": "^6.2.0", "@hyperswarm/dht": "^6.2.0",
"@types/email-templates": "^10.0.1",
"@types/i18n": "^0.13.4",
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6",
"@types/uuid": "^8.3.4",
"apollo-server-express": "^2.25.2", "apollo-server-express": "^2.25.2",
"apollo-server-testing": "^2.25.2",
"axios": "^0.21.1", "axios": "^0.21.1",
"class-validator": "^0.13.1", "class-validator": "^0.13.1",
"cors": "^2.8.5", "cors": "^2.8.5",
@ -46,18 +40,23 @@
"random-bigint": "^0.0.1", "random-bigint": "^0.0.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sodium-native": "^3.3.0", "sodium-native": "^3.3.0",
"ts-jest": "^27.0.5",
"type-graphql": "^1.1.1", "type-graphql": "^1.1.1",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/email-templates": "^10.0.1",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/faker": "^5.5.9", "@types/faker": "^5.5.9",
"@types/i18n": "^0.13.4",
"@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^8.5.2", "@types/jsonwebtoken": "^8.5.2",
"@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^16.10.3", "@types/node": "^16.10.3",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0", "@typescript-eslint/parser": "^4.28.0",
"apollo-server-testing": "^2.25.2",
"eslint": "^7.29.0", "eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",
@ -66,8 +65,10 @@
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0", "eslint-plugin-promise": "^5.1.0",
"faker": "^5.5.3", "faker": "^5.5.3",
"jest": "^27.2.4",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"ts-jest": "^27.0.5",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"tsconfig-paths": "^3.14.0", "tsconfig-paths": "^3.14.0",
"typescript": "^4.3.4" "typescript": "^4.3.4"

View File

@ -17,7 +17,7 @@ const constants = {
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v11.2022-10-27', EXPECTED: 'v12.2022-11-10',
CURRENT: '', CURRENT: '',
}, },
} }
@ -117,7 +117,8 @@ if (
} }
const federation = { const federation = {
DHT_TOPIC: process.env.DHT_TOPIC || null, FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
} }
const CONFIG = { const CONFIG = {

View File

@ -4,11 +4,16 @@
import DHT from '@hyperswarm/dht' import DHT from '@hyperswarm/dht'
// import { Connection } from '@dbTools/typeorm' // import { Connection } from '@dbTools/typeorm'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import CONFIG from '@/config'
function between(min: number, max: number) { function between(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min) return Math.floor(Math.random() * (max - min + 1) + min)
} }
const KEY_SECRET_SEEDBYTES = 32
const getSeed = (): Buffer | null =>
CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null
const POLLTIME = 20000 const POLLTIME = 20000
const SUCCESSTIME = 120000 const SUCCESSTIME = 120000
const ERRORTIME = 240000 const ERRORTIME = 240000
@ -27,8 +32,9 @@ export const startDHT = async (
): Promise<void> => { ): Promise<void> => {
try { try {
const TOPIC = DHT.hash(Buffer.from(topic)) const TOPIC = DHT.hash(Buffer.from(topic))
const keyPair = DHT.keyPair(getSeed())
const keyPair = DHT.keyPair() logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const node = new DHT({ keyPair }) const node = new DHT({ keyPair })

View File

@ -291,7 +291,6 @@ describe('send coins', () => {
await cleanDB() await cleanDB()
}) })
/*
describe('trying to send negative amount', () => { describe('trying to send negative amount', () => {
it('throws an error', async () => { it('throws an error', async () => {
expect( expect(
@ -305,18 +304,15 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], errors: [new GraphQLError(`Amount to send must be positive`)],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(`Amount to send must be positive`)
`user hasn't enough GDD or amount is < 0 : balance=null`,
)
}) })
}) })
*/
describe('good transaction', () => { describe('good transaction', () => {
it('sends the coins', async () => { it('sends the coins', async () => {

View File

@ -314,6 +314,10 @@ export class TransactionResolver {
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`) logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
if (amount.lte(0)) {
logger.error(`Amount to send must be positive`)
throw new Error('Amount to send must be positive')
}
// TODO this is subject to replay attacks // TODO this is subject to replay attacks
const senderUser = getUser(context) const senderUser = getUser(context)
@ -324,22 +328,7 @@ export class TransactionResolver {
// validate recipient user // validate recipient user
const recipientUser = await findUserByEmail(email) const recipientUser = await findUserByEmail(email)
/*
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
if (!emailContact) {
logger.error(`Could not find UserContact with email: ${email}`)
throw new Error(`Could not find UserContact with email: ${email}`)
}
*/
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
/* Code inside this if statement is unreachable (useless by so),
in findUserByEmail() an error is already thrown if the user is not found
*/
if (!recipientUser) {
logger.error(`unknown recipient to UserContact: email=${email}`)
throw new Error('unknown recipient')
}
if (recipientUser.deletedAt) { if (recipientUser.deletedAt) {
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`) logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
throw new Error('The recipient account was deleted') throw new Error('The recipient account was deleted')

View File

@ -170,8 +170,11 @@ describe('util/creation', () => {
const targetDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 0, 0) const targetDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 0, 0)
beforeAll(() => { beforeAll(() => {
const halfMsToRun = (targetDate.getTime() - now.getTime()) / 2
jest.useFakeTimers() jest.useFakeTimers()
setTimeout(jest.fn(), targetDate.getTime() - now.getTime()) setTimeout(jest.fn(), halfMsToRun)
jest.runAllTimers()
setTimeout(jest.fn(), halfMsToRun)
jest.runAllTimers() jest.runAllTimers()
}) })
@ -225,8 +228,10 @@ describe('util/creation', () => {
}) })
it('has the clock set correctly', () => { it('has the clock set correctly', () => {
const targetMonth = nextMonthTargetDate.getMonth() + 1
const targetMonthString = (targetMonth < 10 ? '0' : '') + String(targetMonth)
expect(new Date().toISOString()).toContain( expect(new Date().toISOString()).toContain(
`${nextMonthTargetDate.getFullYear()}-${nextMonthTargetDate.getMonth() + 1}-01T01:`, `${nextMonthTargetDate.getFullYear()}-${targetMonthString}-01T01:`,
) )
}) })

View File

@ -19,8 +19,14 @@ async function main() {
}) })
// start DHT hyperswarm when DHT_TOPIC is set in .env // start DHT hyperswarm when DHT_TOPIC is set in .env
if (CONFIG.DHT_TOPIC) { if (CONFIG.FEDERATION_DHT_TOPIC) {
await startDHT(CONFIG.DHT_TOPIC) // con, // eslint-disable-next-line no-console
console.log(
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
}`,
)
await startDHT(CONFIG.FEDERATION_DHT_TOPIC) // con,
} }
} }

View File

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

View File

@ -26,7 +26,7 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
# backend # backend
BACKEND_CONFIG_VERSION=v11.2022-10-27 BACKEND_CONFIG_VERSION=v12.2022-11-10
JWT_EXPIRES_IN=10m JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net GDT_API_URL=https://gdt.gradido.net
@ -59,10 +59,11 @@ WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol # EventProtocol
EVENT_PROTOCOL_DISABLED=false EVENT_PROTOCOL_DISABLED=false
## DHT # Federation
## if you set this value, the DHT hyperswarm will start to announce and listen # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
## on an hash created from this tpoic # on an hash created from this topic
# DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
# database # database
DATABASE_CONFIG_VERSION=v1.2022-03-18 DATABASE_CONFIG_VERSION=v1.2022-03-18

View File

@ -0,0 +1,379 @@
# Zeitzonen
Die Gradido-Anwendung läuft im Backend in der Zeitzone UTC und im Frontend in der jeweiligen lokalen Zeitzone, in der der User sich anmeldet. Dadurch kann es zu zeitlichen Diskrepanzen kommen, die innerhalb der Anwendungslogik aufgelöst bzw. entsprechend behandelt werden müssen. In den folgenden Kapiteln werden die verschiedenen zeitlichen Konstellationen dargestellt und für die verschiedenen fachlichen Prozesse die daraus resultierenden Problemlösungen beschrieben.
![img](./image/ZeitzonenKonstellationen.png)
## Beispiel 1
Ein User meldet sich in einer Zeitzone t0 - 4 an. Das bedeutet der User liegt 4 Stunden gegenüber der Backend-Zeit zurück.
Konkret hat der User die Zeit 31.08.2022 21:00:00 auf dem Server ist aber die Zeit bei 01.09.2022 01:00:00
Für die Erstellung einer Contribution hat der User noch folgende Gültigkeitsmonate und Beträge zur Wahl:
Juni 2022: 500 GDD | Juli 2022: 200 GDD | August 2022: 1000 GDD
**aber das Backend liefert nur die Beträge, die eigentlich so korrekt wären!!!!!**
**Juli 2022: 200 GDD | August 2022: 1000 GDD | September 2022: 1000 GDD**
Er möchte für den Juni 2022 eine Contribution mit 500 GDD erfassen. **Wird ihm der Juni noch als Schöpfungsmonat angezeigt?**
Falls ja, dann wählt er dabei im FE im Kalender den 30.06.2022. Dann liefert das FE folgende Contribution-Daten an das Backend:
* Gültigkeitsdatum: 30.06.2022 00:00:00
* Memo: text
* Betrag: 500 GDD
* **Zeitzone: wird eine Zeitzone des User aus dem Context geliefert? Das fehlt: entweder über eine Zeit vom FE zum BE und ermitteln Offset im BE**
Im Backend wird dieses dann interpretiert und verarbeitet mit:
* **Belegung des Schöpfungsmonate-Arrays: [ 6, 7, 8] oder [7, 8, 9] da auf dem Server ja schon der 01.09.2022 ist?**
* Gültigkeitsdatum: **30.06.2022 00:00:00 oder 01.07.2022 04:00:00 ?**
* Memo: text
* Betrag 500 GDD
* created_at: 01.07.2022 04:00:00
**Frage: wird die Contribution dem Juni (6) oder dem Juli (7) zugeordnet?**
1. falls Juni zugeordnet kann die Contribution mit 500 GDD eingelöst werden
2. falls Juli zugeordnet muss die Contribution mit 500 GDD abgelehnt werden, da möglicher Schöpfungsbetrag überschritten
## Beispiel 2
Ein User meldet sich in einer Zeitzone t0 + 1 an. Das bedeutet der User liegt 1 Stunde gegenüber der Backend-Zeit voraus.
Konkret hat der User die Zeit 01.09.2022 00:20:00 auf dem Server ist aber die Zeit bei 31.08.2022 23:20:00
Für die Erstellung einer Contribution hat der User noch folgende Gültigkeitsmonate und Beträge zur Wahl:
Juli 2022: 200 GDD | August 2022: 1000 GDD | September 2022: 1000 GDD
**oder wird ihm**
**
Juni 2022: 500 GDD | Juli 2022: 200 GDD | August 2022: 1000 GDD**
**angezeigt, da auf dem BE noch der 31.08.2022 ist?**
Er möchte für den September 2022 eine Contribution mit 500 GDD erfassen und wählt dabei im FE im Kalender den 01.09.2022. Dann liefert das FE folgende Contribution-Daten an das Backend:
* Gültigkeitsdatum: 01.09.2022 00:00:00 (siehe Logauszüge der Fehleranalyse im Ticket #2179)
* Memo: text
* Betrag: 500 GDD
* **Zeitzone: wird eine Zeitzone des User aus dem Context geliefert?**
Im Backend wird dieses dann interpretiert und verarbeitet mit:
* Belegung des Schöpfungsmonate-Arrays: [ 6, 7, 8] **wie kann der User dann aber vorher September 2022 für die Schöpfung auswählen?**
* Gültigkeitsdatum: 01.09.2022 00:00:00
* Memo: text
* Betrag 500 GDD
* created_at: 31.08.2022 23:20:00
Es kommt zu einem **Fehler im Backend**, da im Schöpfungsmonate-Array kein September (9) vorhanden ist, da auf dem Server noch der 31.08.2022 und damit das Array nur die Monate Juni, Juli, August und nicht September beinhaltet.
## Erkenntnisse:
* die dem User angezeigten Schöpfungsmonate errechnen sich aus der lokalen User-Zeit und nicht aus der Backend-Zeit
* das Backend muss somit für Ermittlung der möglichen Schöpfungsmonate und deren noch freien Schöpfungssummen den UserTimeOffset berücksichten
* der gewählte Schöpfungsmonat muss 1:1 vom Frontend in das Backend übertragen werden
* es darf kein Mapping in die Backend-Zeit erfolgen
* sondern es muss der jeweilige UserTimeOffset mitgespeichert werden
* die Logik im BE muss den übertragenen bzw. ermittelten Offset der FE-Zeit entsprechend berücksichten und nicht die Backendzeit in der Logik anwenden
* im BE darf es kein einfaches now = new Date() geben
* im BE muss stattdessen ein userNow = new Date() + UserTimeOffset verwendet werden
* ein CreatedAt / UpdatedAt / DeletedAt / ConfirmedAt wird wie bisher in BE-Zeit gespeichert
* **NEIN nicht notwendig:** plus in einer jeweils neuen Spalte CreatedOffset / UpdatedOffset / DeletedOffset / ConfirmedOffset der dabei gültige UserTimeOffset
* im FE wird immer im Request-Header der aktuelle Zeitpunkt mit Zeitzone geschrieben
*
## Entscheidung
* in den HTTP-Request-Header wird generell der aktuelle Timestamp des Clients eingetragen, sodass die aktuelle Uhrzeit des Users ohne weitere Signatur-Änderungen in jedem Aufruf am Backend ankommt. Moritz erstellt Ticket
* es wird eine Analyse aller Backend-Aufrufe gemacht, die die Auswertung der User-Time und dessen evtl. Timezone-Differenz in der Logik des Backend-Aufrufs benötigt.
* diese Backend-Methoden müssen fachlich so überarbeitet werden, dass immer aus dem Timezone-Offset die korrekte fachliche Logik als Ergebnis heraus kommt. In der Datenbank wird aber immer die UTC-Zeit gespeichert.
* Es werden keine zusätzlichen Datanbank-Attribute zur Speicherung des User-TimeOffsets benötigt.
## Analyse der Backend-Aufrufe
Es werden alle Resolver und ihre Methoden sowie im Resolver exportierte Attribute/Methoden untersucht.
Mit + gekennzeichnet sind diese, die mit dem UserTimeOffset interagieren und überarbeitet werden müssen.
Mit - gekennzeichnet sind diese, die keiner weiteren Aktion bedürfen.
### AdminResolver
#### + adminCreateContribution
Hier wird der User zur übergebenen Email inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und der Initialisierung der Contribution berücksichtigt werden.
#### - adminCreateContributionMessage
nothing to do
#### + adminCreateContributions
Hier wird eine Liste von übergebenen Contributions über den internen Aufruf von *adminCreateContribution()* verarbeitet. Da dort eine Berücksichtigung des User-TimeOffsets notwendig ist, muss hier die UserTime entsprechen im Context weitergereicht werden.
#### - adminDeleteContribution
nothing to do
#### + adminUpdateContribution
analog adminCreateContribution() muss hier der User-TimeOffset berücksichtigt werden.
#### + confirmContribution
Hier wird intern *getUserCreation()* und *validateContribution()* aufgerufen, daher analog adminCreateContribution()
#### + createContributionLink
Hier werden zwar ein *ValidFrom* und ein *ValidTo* Datum übergeben, doch dürften diese keiner Beachtung eines User-TimezoneOffsets unterliegen. Trotzdem bitte noch einmal verifizieren.
#### - creationTransactionList
nothing to do
#### - deleteContributionLink
Es wird zwar der *deletedAt*-Zeitpunkt als Rückgabewert geliefert, doch m.E. dürft hier keine Berücksichtigung des User-TimezoneOffsets notwendig sein.
#### - deleteUser
Es wird zwar der *deletedAt*-Zeitpunkt als Rückgabewert geliefert, doch m.E. dürft hier keine Berücksichtigung des User-TimezoneOffsets notwendig sein.
#### - listContributionLinks
nothing to do
#### + listTransactionLinksAdmin
Hier wird die BE-Zeit für die Suche nach ValidUntil verwendet. Dies sollte nocheinmal verifiziert werden.
#### + listUnconfirmedContributions
Hier wird intern *getUserCreations()* aufgerufen für die Summen der drei Schöpfungsmonate, somit ist der User-TimezoneOffset zu berücksichtigen.
#### + searchUsers
Hier wird intern *getUserCreations()* aufgerufen für die Summen der drei Schöpfungsmonate, somit ist der User-TimezoneOffset zu berücksichtigen.
#### - sendActivationEmail
analog *UserResolver.checkOptInCode*
#### - setUserRole
nothing to do
#### - unDeleteUser
nothing to do
#### + updateContributionLink
Hier werden zwar ein *ValidFrom* und ein *ValidTo* Datum übergeben, doch dürften diese keiner Beachtung eines User-TimezoneOffsets unterliegen. Trotzdem bitte noch einmal verifizieren.
### BalanceResolver
#### + balance
Hier wird der aktuelle Zeitpunkt des BE verwendet, um den Decay und die Summen der Kontostände zu ermitteln. Dies müsste eigentlich von dem User-TimezoneOffset unabhängig sein. Sollte aber noch einmal dahingehend verifiziert werden.
### CommunityResolver
#### - communities
nothing to do
#### - getCommunityInfo
nothing to do
### ContributionMessageResolver
#### - createContributionMessage
nothing to do
#### - listContributionMessages
nothing to do
### ContributionResolver
#### + createContribution
Hier wird der User inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und der Initialisierung der Contribution berücksichtigt werden.
#### - deleteContribution
nothing to do
#### - listAllContributions
nothing to do
#### - listContributions
nothing to do
#### + updateContribution
Hier werden die Contributions des Users inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und dem Update der Contribution berücksichtigt werden.
### GdtResolver
#### - existPid
nothing to do
#### - gdtBalance
nothing to do
#### - listGDTEntries
nothing to do
### KlicktippResolver
nothing to do
### StatisticsResolver
#### + communityStatistics
Hier werden die Daten zum aktuellen BE-Zeitpunkt ermittelt und dem User angezeigt. Aber der User hat ggf. einen anderen TimeOffset. Daher die Frage, ob die Ermittlung der Statistik-Daten mit dem User-TimeOffset stattfinden muss.
### TransactionLinkResolver
#### - transactionLinkCode
nothing to do
#### - transactionLinkExpireDate
nothing to do
#### - createTransactionLink
nothing to do
#### - deleteTransactionLink
nothing to do
#### - listTransactionLinks
nothing to do
#### - queryTransactionLink
nothing to do
#### - redeemTransactionLink
nothing to do
### TransactionResolver
#### - executeTransaction
nothing to do
#### - sendCoins
nothing to do
#### + transactionList
Hier wird der aktuelle BE-Zeitpunkt verwendet, um die Summen der vorhandenen Transactions bis zu diesem Zeitpunkt zu ermitteln. Nach ersten Einschätzungen dürfte es hier nichts zu tun geben. Aber es sollte noch einmal geprüft werden.
### UserResolver
#### - activationLink
nothing to do
#### - checkOptInCode
Hier wird der übergebene OptIn-Code geprüft, ob schon wieder eine erneute Email gesendet werden kann. Die Zeiten werden auf reiner BE-Zeit verglichen, von daher gibt es hier nichts zu tun.
#### - createUser
nothing to do
#### - forgotPassword
In dieser Methode wird am Ende in der Methode *sendResetPasswordEmailMailer()* die Zeit berechnet, wie lange der OptIn-Code im Link gültig ist, default 1440 min oder 24 h.
Es ist keine User-TimeOffset zu berücksichten, da der OptInCode direkt als Parameter im Aufruf von queryOptIn verwendet und dann dort mit der BE-Time verglichen wird.
#### - hasElopage
nothing to do
#### - login
nothing to do
#### - logout
nothing to do
#### - queryOptIn
Hier wird der OptIn-Code aus der *sendResetPasswordEmailMailer()* als Parameter geliefert. Da dessen Gültigkeit zuvor in forgotPassword mit der BE-Zeit gesetzt wurde, benögt man hier keine Berücksichtigung des User-TimeOffsets.
#### - searchAdminUsers
nothing to do
#### - setPassword
nothing to do, analog *queryOptIn*
#### - printTimeDuration
nothing to do
#### - updateUserInfos
nothing to do
#### + verifyLogin
Hier wird der User inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contribtutions, egal ob bestätigt oder noch offen ermittelt.
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.

View File

@ -0,0 +1,217 @@
<mxfile host="65bd71144e">
<diagram id="-PxXzgsMUT8aslXVdGG0" name="Seite-1">
<mxGraphModel dx="1022" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="" style="endArrow=classic;html=1;strokeWidth=5;fillColor=#60a917;strokeColor=#2D7600;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="160" y="320" as="sourcePoint"/>
<mxPoint x="2160" y="320" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="3" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1160" y="360" as="sourcePoint"/>
<mxPoint x="1160" y="320" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="4" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1130" y="370" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="5" value="Backend&lt;br style=&quot;font-size: 18px;&quot;&gt;Zeitzone UTC" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="40" y="360" width="200" height="30" as="geometry"/>
</mxCell>
<mxCell id="6" value="Frontend&lt;br style=&quot;font-size: 18px;&quot;&gt;verschiedene Zeitzonen" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="40" y="240" width="240" height="30" as="geometry"/>
</mxCell>
<mxCell id="7" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1160" y="320" as="sourcePoint"/>
<mxPoint x="1160" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="8" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1080" y="320" as="sourcePoint"/>
<mxPoint x="1080" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="9" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1000" y="320" as="sourcePoint"/>
<mxPoint x="1000" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="10" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="920" y="320" as="sourcePoint"/>
<mxPoint x="920" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="11" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="840" y="320" as="sourcePoint"/>
<mxPoint x="840" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="12" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="760" y="320" as="sourcePoint"/>
<mxPoint x="760" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="13" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="680" y="320" as="sourcePoint"/>
<mxPoint x="680" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="14" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="600" y="320" as="sourcePoint"/>
<mxPoint x="600" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="15" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="520" y="320" as="sourcePoint"/>
<mxPoint x="520" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="16" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="440" y="320" as="sourcePoint"/>
<mxPoint x="440" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="17" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="360" y="320" as="sourcePoint"/>
<mxPoint x="360" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="18" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="279.75" y="320" as="sourcePoint"/>
<mxPoint x="279.75" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="19" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="200" y="320" as="sourcePoint"/>
<mxPoint x="200" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="20" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="2120" y="320" as="sourcePoint"/>
<mxPoint x="2120" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="21" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="2040" y="320" as="sourcePoint"/>
<mxPoint x="2040" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="22" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1960" y="320" as="sourcePoint"/>
<mxPoint x="1960" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="23" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1880" y="320" as="sourcePoint"/>
<mxPoint x="1880" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1800" y="320" as="sourcePoint"/>
<mxPoint x="1800" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="25" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1720" y="320" as="sourcePoint"/>
<mxPoint x="1720" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="26" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1640" y="320" as="sourcePoint"/>
<mxPoint x="1640" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="27" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1560" y="320" as="sourcePoint"/>
<mxPoint x="1560" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="28" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1480" y="320" as="sourcePoint"/>
<mxPoint x="1480" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="29" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1400" y="320" as="sourcePoint"/>
<mxPoint x="1400" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="30" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1319.75" y="320" as="sourcePoint"/>
<mxPoint x="1319.75" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="31" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1240" y="320" as="sourcePoint"/>
<mxPoint x="1240" y="280" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="32" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1130" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="33" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt; - 2&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="970" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="36" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt; - 4&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="810" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="38" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt; - 6&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="650" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="40" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt; - 8&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="490" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="42" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt; - 10&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="330" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="43" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt;&amp;nbsp;+ 10&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1930" y="240" width="70" height="30" as="geometry"/>
</mxCell>
<mxCell id="44" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt;&amp;nbsp;+ 8&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1770" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="45" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt;&amp;nbsp;+ 6&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1610" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="46" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt;&amp;nbsp;+ 4&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1450" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="47" value="&lt;font style=&quot;font-size: 24px&quot;&gt;&lt;b&gt;t&lt;/b&gt;&lt;/font&gt;&lt;font size=&quot;1&quot;&gt;&lt;b style=&quot;font-size: 13px&quot;&gt;0&lt;/b&gt;&lt;b style=&quot;font-size: 20px&quot;&gt;&amp;nbsp;+ 2&lt;/b&gt;&lt;/font&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1290" y="240" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="89" value="mögliche Zeitzonen-Konstellationen" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=22;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="40" y="130" width="400" height="30" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

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

View File

@ -38,10 +38,12 @@ export default {
form: { form: {
text: '', text: '',
}, },
isSubmitting: false,
} }
}, },
methods: { methods: {
onSubmit() { onSubmit() {
this.isSubmitting = true
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: createContributionMessage, mutation: createContributionMessage,
@ -55,9 +57,11 @@ export default {
this.$emit('update-state', this.contributionId) this.$emit('update-state', this.contributionId)
this.form.text = '' this.form.text = ''
this.toastSuccess(this.$t('message.reply')) this.toastSuccess(this.$t('message.reply'))
this.isSubmitting = false
}) })
.catch((error) => { .catch((error) => {
this.toastError(error.message) this.toastError(error.message)
this.isSubmitting = false
}) })
}, },
onReset() { onReset() {
@ -66,10 +70,7 @@ export default {
}, },
computed: { computed: {
disabled() { disabled() {
if (this.form.text !== '') { return this.form.text === '' || this.isSubmitting
return false
}
return true
}, },
}, },
} }

View File

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