mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2285-mark-creation-via-link
This commit is contained in:
commit
96fd4cb14c
13
CHANGELOG.md
13
CHANGELOG.md
@ -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).
|
||||
|
||||
#### [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)
|
||||
|
||||
> 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)
|
||||
|
||||
#### [1.14.0](https://github.com/gradido/gradido/compare/1.13.3...1.14.0)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.14.1",
|
||||
"version": "1.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -38,10 +38,12 @@ export default {
|
||||
form: {
|
||||
text: '',
|
||||
},
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit(event) {
|
||||
this.loading = true
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: adminCreateContributionMessage,
|
||||
@ -55,9 +57,11 @@ export default {
|
||||
this.$emit('update-state', this.contributionId)
|
||||
this.form.text = ''
|
||||
this.toastSuccess(this.$t('message.request'))
|
||||
this.loading = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
onReset(event) {
|
||||
@ -66,10 +70,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
disabled() {
|
||||
if (this.form.text !== '') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return this.form.text === '' || this.loading
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v11.2022-10-27
|
||||
CONFIG_VERSION=v12.2022-11-10
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
@ -61,7 +61,8 @@ EVENT_PROTOCOL_DISABLED=false
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
# LOG_LEVEL=info
|
||||
|
||||
# DHT
|
||||
# if you set this value, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this tpoic
|
||||
# DHT_TOPIC=GRADIDO_HUB
|
||||
# Federation
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
|
||||
@ -56,5 +56,6 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
|
||||
|
||||
# DHT
|
||||
DHT_TOPIC=$DHT_TOPIC
|
||||
# Federation
|
||||
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
||||
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
||||
|
||||
@ -98,10 +98,18 @@ COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
|
||||
# We also copy the node_modules express and serve-static for the run script
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules
|
||||
|
||||
# Copy static files
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
|
||||
# Copy package.json for script definitions (lock file should not be needed)
|
||||
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
|
||||
# Copy tsconfig.json to provide alias path definitions
|
||||
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
|
||||
# Copy log4js-config.json to provide log configuration
|
||||
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json
|
||||
# Copy memonic type since its referenced in the sources
|
||||
# TODO: remove
|
||||
COPY --from=build ${DOCKER_WORKDIR}/src/config/mnemonic.uncompressed_buffer13116.txt ./src/config/mnemonic.uncompressed_buffer13116.txt
|
||||
# Copy run scripts run/
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.14.1",
|
||||
"version": "1.15.0",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -19,13 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@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-testing": "^2.25.2",
|
||||
"axios": "^0.21.1",
|
||||
"class-validator": "^0.13.1",
|
||||
"cors": "^2.8.5",
|
||||
@ -46,18 +40,23 @@
|
||||
"random-bigint": "^0.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sodium-native": "^3.3.0",
|
||||
"ts-jest": "^27.0.5",
|
||||
"type-graphql": "^1.1.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/email-templates": "^10.0.1",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/faker": "^5.5.9",
|
||||
"@types/i18n": "^0.13.4",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/jsonwebtoken": "^8.5.2",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"apollo-server-testing": "^2.25.2",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
@ -66,8 +65,10 @@
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"faker": "^5.5.3",
|
||||
"jest": "^27.2.4",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^2.3.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "^3.14.0",
|
||||
"typescript": "^4.3.4"
|
||||
|
||||
@ -10,14 +10,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0052-add_updated_at_to_contributions',
|
||||
DB_VERSION: '0054-recalculate_balance_and_decay',
|
||||
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: 'v11.2022-10-27',
|
||||
EXPECTED: 'v12.2022-11-10',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -117,7 +117,8 @@ if (
|
||||
}
|
||||
|
||||
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 = {
|
||||
|
||||
@ -4,11 +4,16 @@
|
||||
import DHT from '@hyperswarm/dht'
|
||||
// import { Connection } from '@dbTools/typeorm'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
function between(min: number, max: number) {
|
||||
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 SUCCESSTIME = 120000
|
||||
const ERRORTIME = 240000
|
||||
@ -27,8 +32,9 @@ export const startDHT = async (
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const TOPIC = DHT.hash(Buffer.from(topic))
|
||||
|
||||
const keyPair = DHT.keyPair()
|
||||
const keyPair = DHT.keyPair(getSeed())
|
||||
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
|
||||
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
|
||||
|
||||
const node = new DHT({ keyPair })
|
||||
|
||||
|
||||
12
backend/src/graphql/enum/PasswordEncryptionType.ts
Normal file
12
backend/src/graphql/enum/PasswordEncryptionType.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum PasswordEncryptionType {
|
||||
NO_PASSWORD = 0,
|
||||
EMAIL = 1,
|
||||
GRADIDO_ID = 2,
|
||||
}
|
||||
|
||||
registerEnumType(PasswordEncryptionType, {
|
||||
name: 'PasswordEncryptionType', // this one is mandatory
|
||||
description: 'Type of the password encryption', // this one is optional
|
||||
})
|
||||
@ -74,7 +74,10 @@ export class TransactionLinkResolver {
|
||||
const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
|
||||
|
||||
// validate amount
|
||||
await calculateBalance(user.id, holdAvailableAmount, createdDate)
|
||||
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
|
||||
if (!sendBalance) {
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
|
||||
const transactionLink = dbTransactionLink.create()
|
||||
transactionLink.userId = user.id
|
||||
|
||||
@ -16,7 +16,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { User } from '@entity/User'
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { logger } from '@test/testSetup'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
@ -253,50 +253,21 @@ describe('send coins', () => {
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`User has not received any GDD yet`)],
|
||||
errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`No prior transaction found for user with id: ${user[1].id}`,
|
||||
`user hasn't enough GDD or amount is < 0 : balance=null`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sending negative amount', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: -50,
|
||||
memo: 'testing negative',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Transaction amount must be greater than 0')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Transaction amount must be greater than 0: -50')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has some GDD', () => {
|
||||
beforeAll(async () => {
|
||||
resetToken()
|
||||
|
||||
// login as bob again
|
||||
await query({ mutation: login, variables: bobData })
|
||||
|
||||
// create contribution as user bob
|
||||
const contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
@ -316,6 +287,33 @@ describe('send coins', () => {
|
||||
await query({ mutation: login, variables: bobData })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
describe('trying to send negative amount', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: -50,
|
||||
memo: 'testing negative',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`Amount to send must be positive`)],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`Amount to send must be positive`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('good transaction', () => {
|
||||
it('sends the coins', async () => {
|
||||
expect(
|
||||
|
||||
@ -39,7 +39,6 @@ import { findUserByEmail } from './UserResolver'
|
||||
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
||||
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import { Decay } from '../model/Decay'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
@ -69,8 +68,17 @@ export const executeTransaction = async (
|
||||
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
|
||||
const sendBalance = await calculateBalance(sender.id, amount, receivedCallDate, transactionLink)
|
||||
const sendBalance = await calculateBalance(
|
||||
sender.id,
|
||||
amount.mul(-1),
|
||||
receivedCallDate,
|
||||
transactionLink,
|
||||
)
|
||||
logger.debug(`calculated Balance=${sendBalance}`)
|
||||
if (!sendBalance) {
|
||||
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
@ -100,24 +108,7 @@ export const executeTransaction = async (
|
||||
transactionReceive.userId = recipient.id
|
||||
transactionReceive.linkedUserId = sender.id
|
||||
transactionReceive.amount = amount
|
||||
|
||||
// state received balance
|
||||
let receiveBalance: {
|
||||
balance: Decimal
|
||||
decay: Decay
|
||||
lastTransactionId: number
|
||||
} | null
|
||||
|
||||
// try received balance
|
||||
try {
|
||||
receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`User with no transactions sent: ${recipient.id}, has received a transaction of ${amount} GDD from user: ${sender.id}`,
|
||||
)
|
||||
receiveBalance = null
|
||||
}
|
||||
|
||||
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)
|
||||
@ -322,6 +313,10 @@ export class TransactionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
|
||||
if (amount.lte(0)) {
|
||||
logger.error(`Amount to send must be positive`)
|
||||
throw new Error('Amount to send must be positive')
|
||||
}
|
||||
|
||||
// TODO this is subject to replay attacks
|
||||
const senderUser = getUser(context)
|
||||
@ -332,22 +327,7 @@ export class TransactionResolver {
|
||||
|
||||
// validate recipient user
|
||||
const recipientUser = await findUserByEmail(email)
|
||||
/*
|
||||
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find UserContact with email: ${email}`)
|
||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||
}
|
||||
*/
|
||||
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
||||
|
||||
/* Code inside this if statement is unreachable (useless by so),
|
||||
in findUserByEmail() an error is already thrown if the user is not found
|
||||
*/
|
||||
if (!recipientUser) {
|
||||
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||
throw new Error('unknown recipient')
|
||||
}
|
||||
if (recipientUser.deletedAt) {
|
||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||
throw new Error('The recipient account was deleted')
|
||||
|
||||
@ -36,6 +36,9 @@ import { UserContact } from '@entity/UserContact'
|
||||
import { OptInType } from '../enum/OptInType'
|
||||
import { UserContactType } from '../enum/UserContactType'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { encryptPassword } from '@/password/PasswordEncryptor'
|
||||
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
|
||||
import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
|
||||
|
||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
|
||||
@ -146,6 +149,7 @@ describe('UserResolver', () => {
|
||||
publisherId: 1234,
|
||||
referrerId: null,
|
||||
contributionLinkId: null,
|
||||
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
|
||||
},
|
||||
])
|
||||
const valUUID = validateUUID(user[0].gradidoID)
|
||||
@ -491,7 +495,8 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('updates the password', () => {
|
||||
expect(newUser.password).toEqual('3917921995996627700')
|
||||
const encryptedPass = encryptPassword(newUser, 'Aa12345_')
|
||||
expect(newUser.password.toString()).toEqual(encryptedPass.toString())
|
||||
})
|
||||
|
||||
/*
|
||||
@ -1159,6 +1164,93 @@ describe('UserResolver', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('password encryption type', () => {
|
||||
describe('user just registered', () => {
|
||||
let bibi: User
|
||||
|
||||
it('has password type gradido id', async () => {
|
||||
const users = await User.find()
|
||||
bibi = users[1]
|
||||
|
||||
expect(bibi).toEqual(
|
||||
expect.objectContaining({
|
||||
password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0]
|
||||
.readBigUInt64LE()
|
||||
.toString(),
|
||||
passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has encryption type email', () => {
|
||||
const variables = {
|
||||
email: 'bibi@bloxberg.de',
|
||||
password: 'Aa12345_',
|
||||
publisherId: 1234,
|
||||
}
|
||||
|
||||
let bibi: User
|
||||
beforeAll(async () => {
|
||||
const usercontact = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
bibi = usercontact.user
|
||||
bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL
|
||||
bibi.password = SecretKeyCryptographyCreateKey(
|
||||
'bibi@bloxberg.de',
|
||||
'Aa12345_',
|
||||
)[0].readBigUInt64LE()
|
||||
|
||||
await bibi.save()
|
||||
})
|
||||
|
||||
it('changes to gradidoID on login', async () => {
|
||||
await mutate({ mutation: login, variables: variables })
|
||||
|
||||
const usercontact = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
bibi = usercontact.user
|
||||
|
||||
expect(bibi).toEqual(
|
||||
expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0]
|
||||
.readBigUInt64LE()
|
||||
.toString(),
|
||||
passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('can login after password change', async () => {
|
||||
resetToken()
|
||||
expect(await mutate({ mutation: login, variables: variables })).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
login: {
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
hasElopage: false,
|
||||
id: expect.any(Number),
|
||||
isAdmin: null,
|
||||
klickTipp: {
|
||||
newsletterState: false,
|
||||
},
|
||||
language: 'de',
|
||||
lastName: 'Bloxberg',
|
||||
publisherId: 1234,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('printTimeDuration', () => {
|
||||
|
||||
@ -40,17 +40,15 @@ import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||
import Paginated from '@arg/Paginated'
|
||||
import { Order } from '@enum/Order'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
|
||||
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
|
||||
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const sodium = require('sodium-native')
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const random = require('random-bigint')
|
||||
|
||||
// We will reuse this for changePassword
|
||||
const isPassword = (password: string): boolean => {
|
||||
return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/)
|
||||
}
|
||||
|
||||
const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
|
||||
const DEFAULT_LANGUAGE = 'de'
|
||||
const isLanguage = (language: string): boolean => {
|
||||
@ -107,48 +105,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
|
||||
return [pubKey, privKey]
|
||||
}
|
||||
|
||||
const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
|
||||
logger.trace('SecretKeyCryptographyCreateKey...')
|
||||
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
||||
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
||||
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
|
||||
logger.error(
|
||||
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
|
||||
)
|
||||
throw new Error(
|
||||
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
|
||||
)
|
||||
}
|
||||
|
||||
const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES)
|
||||
sodium.crypto_hash_sha512_init(state)
|
||||
sodium.crypto_hash_sha512_update(state, Buffer.from(salt))
|
||||
sodium.crypto_hash_sha512_update(state, configLoginAppSecret)
|
||||
const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES)
|
||||
sodium.crypto_hash_sha512_final(state, hash)
|
||||
|
||||
const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES)
|
||||
const opsLimit = 10
|
||||
const memLimit = 33554432
|
||||
const algo = 2
|
||||
sodium.crypto_pwhash(
|
||||
encryptionKey,
|
||||
Buffer.from(password),
|
||||
hash.slice(0, sodium.crypto_pwhash_SALTBYTES),
|
||||
opsLimit,
|
||||
memLimit,
|
||||
algo,
|
||||
)
|
||||
|
||||
const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
|
||||
sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
|
||||
|
||||
logger.debug(
|
||||
`SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`,
|
||||
)
|
||||
return [encryptionKeyHash, encryptionKey]
|
||||
}
|
||||
|
||||
/*
|
||||
const getEmailHash = (email: string): Buffer => {
|
||||
logger.trace('getEmailHash...')
|
||||
@ -346,12 +302,17 @@ 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 private or publicKey')
|
||||
}
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||
const loginUserPassword = BigInt(dbUser.password.toString())
|
||||
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
|
||||
|
||||
if (!verifyPassword(dbUser, password)) {
|
||||
logger.error('The User has no valid credentials.')
|
||||
throw new Error('No user with this credentials')
|
||||
}
|
||||
|
||||
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
dbUser.password = encryptPassword(dbUser, password)
|
||||
await dbUser.save()
|
||||
}
|
||||
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
|
||||
logger.addContext('user', dbUser.id)
|
||||
logger.debug('validation of login credentials successful...')
|
||||
@ -481,6 +442,7 @@ export class UserResolver {
|
||||
dbUser.lastName = lastName
|
||||
dbUser.language = language
|
||||
dbUser.publisherId = publisherId
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
|
||||
dbUser.passphrase = passphrase.join(' ')
|
||||
logger.debug('new dbUser=' + dbUser)
|
||||
if (redeemCode) {
|
||||
@ -634,7 +596,7 @@ export class UserResolver {
|
||||
): Promise<boolean> {
|
||||
logger.info(`setPassword(${code}, ***)...`)
|
||||
// Validate Password
|
||||
if (!isPassword(password)) {
|
||||
if (!isValidPassword(password)) {
|
||||
logger.error('Password entered is lexically invalid')
|
||||
throw new Error(
|
||||
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||
@ -692,10 +654,11 @@ export class UserResolver {
|
||||
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 = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||
user.password = encryptPassword(user, password)
|
||||
user.pubKey = keyPair[0]
|
||||
user.privKey = encryptedPrivkey
|
||||
logger.debug('User credentials updated ...')
|
||||
@ -801,7 +764,7 @@ export class UserResolver {
|
||||
|
||||
if (password && passwordNew) {
|
||||
// Validate Password
|
||||
if (!isPassword(passwordNew)) {
|
||||
if (!isValidPassword(passwordNew)) {
|
||||
logger.error('newPassword does not fullfil the rules')
|
||||
throw new Error(
|
||||
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||
@ -813,7 +776,7 @@ export class UserResolver {
|
||||
userEntity.emailContact.email,
|
||||
password,
|
||||
)
|
||||
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||
if (!verifyPassword(userEntity, password)) {
|
||||
logger.error(`Old password is invalid`)
|
||||
throw new Error(`Old password is invalid`)
|
||||
}
|
||||
@ -829,7 +792,8 @@ export class UserResolver {
|
||||
logger.debug('PrivateKey encrypted...')
|
||||
|
||||
// Save new password hash and newly encrypted private key
|
||||
userEntity.password = newPasswordHash[0].readBigUInt64LE()
|
||||
userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
userEntity.password = encryptPassword(userEntity, passwordNew)
|
||||
userEntity.privKey = encryptedPrivkey
|
||||
}
|
||||
|
||||
|
||||
@ -19,8 +19,14 @@ async function main() {
|
||||
})
|
||||
|
||||
// start DHT hyperswarm when DHT_TOPIC is set in .env
|
||||
if (CONFIG.DHT_TOPIC) {
|
||||
await startDHT(CONFIG.DHT_TOPIC) // con,
|
||||
if (CONFIG.FEDERATION_DHT_TOPIC) {
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
71
backend/src/password/EncryptorUtils.ts
Normal file
71
backend/src/password/EncryptorUtils.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import CONFIG from '@/config'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { User } from '@entity/User'
|
||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const sodium = require('sodium-native')
|
||||
|
||||
// We will reuse this for changePassword
|
||||
export const isValidPassword = (password: string): boolean => {
|
||||
return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/)
|
||||
}
|
||||
|
||||
export const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
|
||||
logger.trace('SecretKeyCryptographyCreateKey...')
|
||||
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
||||
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
||||
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
|
||||
logger.error(
|
||||
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
|
||||
)
|
||||
throw new Error(
|
||||
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
|
||||
)
|
||||
}
|
||||
|
||||
const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES)
|
||||
sodium.crypto_hash_sha512_init(state)
|
||||
sodium.crypto_hash_sha512_update(state, Buffer.from(salt))
|
||||
sodium.crypto_hash_sha512_update(state, configLoginAppSecret)
|
||||
const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES)
|
||||
sodium.crypto_hash_sha512_final(state, hash)
|
||||
|
||||
const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES)
|
||||
const opsLimit = 10
|
||||
const memLimit = 33554432
|
||||
const algo = 2
|
||||
sodium.crypto_pwhash(
|
||||
encryptionKey,
|
||||
Buffer.from(password),
|
||||
hash.slice(0, sodium.crypto_pwhash_SALTBYTES),
|
||||
opsLimit,
|
||||
memLimit,
|
||||
algo,
|
||||
)
|
||||
|
||||
const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
|
||||
sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
|
||||
|
||||
return [encryptionKeyHash, encryptionKey]
|
||||
}
|
||||
|
||||
export const getUserCryptographicSalt = (dbUser: User): string => {
|
||||
switch (dbUser.passwordEncryptionType) {
|
||||
case PasswordEncryptionType.NO_PASSWORD: {
|
||||
logger.error('Password not set for user ' + dbUser.id)
|
||||
throw new Error('Password not set for user ' + dbUser.id) // user has no password
|
||||
}
|
||||
case PasswordEncryptionType.EMAIL: {
|
||||
return dbUser.emailContact.email
|
||||
break
|
||||
}
|
||||
case PasswordEncryptionType.GRADIDO_ID: {
|
||||
return dbUser.gradidoID
|
||||
break
|
||||
}
|
||||
default:
|
||||
logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
|
||||
throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
|
||||
}
|
||||
}
|
||||
14
backend/src/password/PasswordEncryptor.ts
Normal file
14
backend/src/password/PasswordEncryptor.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { User } from '@entity/User'
|
||||
// import { logger } from '@test/testSetup' getting error "jest is not defined"
|
||||
import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils'
|
||||
|
||||
export const encryptPassword = (dbUser: User, password: string): bigint => {
|
||||
const salt = getUserCryptographicSalt(dbUser)
|
||||
const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash
|
||||
const passwordHash = keyBuffer[0].readBigUInt64LE()
|
||||
return passwordHash
|
||||
}
|
||||
|
||||
export const verifyPassword = (dbUser: User, password: string): boolean => {
|
||||
return dbUser.password.toString() === encryptPassword(dbUser, password).toString()
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType'
|
||||
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
@ -26,6 +27,8 @@ const communityDbUser: dbUser = {
|
||||
isAdmin: null,
|
||||
publisherId: 0,
|
||||
passphrase: '',
|
||||
// default password encryption type
|
||||
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
|
||||
hasId: function (): boolean {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
|
||||
@ -1,17 +1,5 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => {
|
||||
return Object.keys(obj).map(function (key) {
|
||||
return obj[key]
|
||||
})
|
||||
}
|
||||
|
||||
// to improve code readability, as String is needed, it is handled inside this utility function
|
||||
export const decimalAddition = (a: Decimal, b: Decimal): Decimal => {
|
||||
return a.add(b.toString())
|
||||
}
|
||||
|
||||
// to improve code readability, as String is needed, it is handled inside this utility function
|
||||
export const decimalSubtraction = (a: Decimal, b: Decimal): Decimal => {
|
||||
return a.minus(b.toString())
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@ import { Decay } from '@model/Decay'
|
||||
import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { decimalSubtraction, decimalAddition } from './utilities'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
function isStringBoolean(value: string): boolean {
|
||||
const lowerValue = value.toLowerCase()
|
||||
@ -25,26 +23,13 @@ async function calculateBalance(
|
||||
amount: Decimal,
|
||||
time: Date,
|
||||
transactionLink?: dbTransactionLink | null,
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number }> {
|
||||
// negative or empty amount should not be allowed
|
||||
if (amount.lessThanOrEqualTo(0)) {
|
||||
logger.error(`Transaction amount must be greater than 0: ${amount}`)
|
||||
throw new Error('Transaction amount must be greater than 0')
|
||||
}
|
||||
|
||||
// check if user has prior transactions
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
||||
|
||||
if (!lastTransaction) {
|
||||
logger.error(`No prior transaction found for user with id: ${userId}`)
|
||||
throw new Error('User has not received any GDD yet')
|
||||
}
|
||||
if (!lastTransaction) return null
|
||||
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||
|
||||
// new balance is the old balance minus the amount used
|
||||
const balance = decimalSubtraction(decay.balance, amount)
|
||||
|
||||
const balance = decay.balance.add(amount.toString())
|
||||
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
||||
const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time)
|
||||
|
||||
@ -52,16 +37,11 @@ async function calculateBalance(
|
||||
// else we cannot redeem links which are more or equal to half of what an account actually owns
|
||||
const releasedLinkAmount = transactionLink ? transactionLink.holdAvailableAmount : new Decimal(0)
|
||||
|
||||
const availableBalance = decimalSubtraction(balance, sumHoldAvailableAmount)
|
||||
|
||||
if (decimalAddition(availableBalance, releasedLinkAmount).lessThan(0)) {
|
||||
logger.error(
|
||||
`Not enough funds for a transaction of ${amount} GDD, user with id: ${userId} has only ${balance} GDD available`,
|
||||
)
|
||||
throw new Error('Not enough funds for transaction')
|
||||
if (
|
||||
balance.minus(sumHoldAvailableAmount.toString()).plus(releasedLinkAmount.toString()).lessThan(0)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
logger.debug(`calculated Balance=${balance}`)
|
||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||
}
|
||||
|
||||
|
||||
@ -100,6 +100,8 @@ COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
|
||||
# Copy Mnemonic files
|
||||
COPY --from=build ${DOCKER_WORKDIR}/src/config/*.txt ./src/config/
|
||||
# Copy log folder
|
||||
COPY --from=build ${DOCKER_WORKDIR}/log ./log
|
||||
# Copy run scripts run/
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
|
||||
|
||||
|
||||
127
database/entity/0053-change_password_encryption/User.ts
Normal file
127
database/entity/0053-change_password_encryption/User.ts
Normal file
@ -0,0 +1,127 @@
|
||||
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
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||
pubKey: Buffer
|
||||
|
||||
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||
privKey: Buffer
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
name: 'passphrase',
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
passphrase: 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[]
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
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_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||
// emailHash: Buffer
|
||||
|
||||
@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 { User } from './0049-add_user_contacts_table/User'
|
||||
export { User } from './0053-change_password_encryption/User'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { UserContact } from './0049-add_user_contacts_table/UserContact'
|
||||
export { UserContact } from './0053-change_password_encryption/UserContact'
|
||||
|
||||
2
database/log/.gitignore
vendored
Normal file
2
database/log/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
24
database/migrations/0053-change_password_encryption.ts
Normal file
24
database/migrations/0053-change_password_encryption.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/* MIGRATION TO ADD ENCRYPTION TYPE TO PASSWORDS
|
||||
*
|
||||
* This migration adds and renames columns in the table `users`
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE users RENAME COLUMN created TO created_at;')
|
||||
await queryFn('ALTER TABLE users RENAME COLUMN deletedAt TO deleted_at;')
|
||||
// alter table emp rename column emp_name to name
|
||||
await queryFn(
|
||||
'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;',
|
||||
)
|
||||
await queryFn(`UPDATE users SET password_encryption_type = 1 WHERE id IN
|
||||
(SELECT user_id FROM user_contacts WHERE email_checked = 1)`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE users RENAME COLUMN created_at TO created;')
|
||||
await queryFn('ALTER TABLE users RENAME COLUMN deleted_at TO deletedAt;')
|
||||
await queryFn('ALTER TABLE users DROP COLUMN password_encryption_type;')
|
||||
}
|
||||
160
database/migrations/0054-recalculate_balance_and_decay.ts
Normal file
160
database/migrations/0054-recalculate_balance_and_decay.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/* MIGRATION TO FIX WRONG BALANCE
|
||||
*
|
||||
* Due to a bug in the code
|
||||
* the amount of a receive balance is substracted
|
||||
* from the previous balance instead of added.
|
||||
*
|
||||
* Therefore all balance and decay fields must
|
||||
* be recalculated
|
||||
*
|
||||
* WARNING: This Migration must be run in TZ=UTC
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import fs from 'fs'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
// Set precision value
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
|
||||
const DECAY_START_TIME = new Date('2021-05-13 17:46:31') // GMT+0
|
||||
|
||||
interface Decay {
|
||||
balance: Decimal
|
||||
decay: Decimal | null
|
||||
start: Date | null
|
||||
end: Date | null
|
||||
duration: number | null
|
||||
}
|
||||
|
||||
export enum TransactionTypeId {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
}
|
||||
|
||||
function decayFormula(value: Decimal, seconds: number): Decimal {
|
||||
return value.mul(new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds))
|
||||
}
|
||||
|
||||
function calculateDecay(
|
||||
amount: Decimal,
|
||||
from: Date,
|
||||
to: Date,
|
||||
startBlock: Date = DECAY_START_TIME,
|
||||
): Decay {
|
||||
const fromMs = from.getTime()
|
||||
const toMs = to.getTime()
|
||||
const startBlockMs = startBlock.getTime()
|
||||
|
||||
if (toMs < fromMs) {
|
||||
throw new Error('to < from, reverse decay calculation is invalid')
|
||||
}
|
||||
|
||||
// Initialize with no decay
|
||||
const decay: Decay = {
|
||||
balance: amount,
|
||||
decay: null,
|
||||
start: null,
|
||||
end: null,
|
||||
duration: null,
|
||||
}
|
||||
|
||||
// decay started after end date; no decay
|
||||
if (startBlockMs > toMs) {
|
||||
return decay
|
||||
}
|
||||
// decay started before start date; decay for full duration
|
||||
if (startBlockMs < fromMs) {
|
||||
decay.start = from
|
||||
decay.duration = (toMs - fromMs) / 1000
|
||||
}
|
||||
// decay started between start and end date; decay from decay start till end date
|
||||
else {
|
||||
decay.start = startBlock
|
||||
decay.duration = (toMs - startBlockMs) / 1000
|
||||
}
|
||||
|
||||
decay.end = to
|
||||
decay.balance = decayFormula(amount, decay.duration)
|
||||
decay.decay = decay.balance.minus(amount)
|
||||
return decay
|
||||
}
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// Write log file
|
||||
const logFile = 'log/0054-recalculate_balance_and_decay.log.csv'
|
||||
await fs.writeFile(
|
||||
logFile,
|
||||
`email;first_name;last_name;affected_transactions;new_balance;new_decay;old_balance;old_decay;delta;\n`,
|
||||
(err) => {
|
||||
if (err) throw err
|
||||
},
|
||||
)
|
||||
|
||||
// Find all users & loop over them
|
||||
const users = await queryFn('SELECT user_id FROM transactions GROUP BY user_id;')
|
||||
for (let u = 0; u < users.length; u++) {
|
||||
const userId = users[u].user_id
|
||||
// find all transactions for a user
|
||||
const transactions = await queryFn(
|
||||
`SELECT *, CONVERT(balance, CHAR) as dec_balance, CONVERT(decay, CHAR) as dec_decay FROM transactions WHERE user_id = ${userId} ORDER BY balance_date ASC;`,
|
||||
)
|
||||
|
||||
let previous = null
|
||||
let affectedTransactions = 0
|
||||
let balance = new Decimal(0)
|
||||
for (let t = 0; t < transactions.length; t++) {
|
||||
const transaction = transactions[t]
|
||||
const decayStartDate = previous ? previous.balance_date : transaction.balance_date
|
||||
const amount = new Decimal(transaction.amount)
|
||||
const decay = calculateDecay(balance, decayStartDate, transaction.balance_date)
|
||||
balance = decay.balance.add(amount)
|
||||
|
||||
const userContact = await queryFn(
|
||||
`SELECT email, first_name, last_name FROM users LEFT JOIN user_contacts ON users.email_id = user_contacts.id WHERE users.id = ${userId}`,
|
||||
)
|
||||
const userEmail = userContact.length === 1 ? userContact[0].email : userId
|
||||
const userFirstName = userContact.length === 1 ? userContact[0].first_name : ''
|
||||
const userLastName = userContact.length === 1 ? userContact[0].last_name : ''
|
||||
|
||||
// Update if needed
|
||||
if (!balance.eq(transaction.dec_balance)) {
|
||||
await queryFn(`
|
||||
UPDATE transactions SET
|
||||
balance = ${balance},
|
||||
decay = ${decay.decay ? decay.decay : 0}
|
||||
WHERE id = ${transaction.id};
|
||||
`)
|
||||
affectedTransactions++
|
||||
|
||||
// Log on last entry
|
||||
if (t === transactions.length - 1) {
|
||||
fs.appendFile(
|
||||
logFile,
|
||||
`${userEmail};${userFirstName};${userLastName};${affectedTransactions};${balance};${
|
||||
decay.decay ? decay.decay : 0
|
||||
};${transaction.dec_balance};${transaction.dec_decay};${balance.sub(
|
||||
transaction.dec_balance,
|
||||
)};\n`,
|
||||
(err) => {
|
||||
if (err) throw err
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// previous
|
||||
previous = transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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>>) {}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "1.14.1",
|
||||
"version": "1.15.0",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
|
||||
@ -26,7 +26,7 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v11.2022-10-27
|
||||
BACKEND_CONFIG_VERSION=v12.2022-11-10
|
||||
|
||||
JWT_EXPIRES_IN=10m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
@ -59,10 +59,11 @@ WEBHOOK_ELOPAGE_SECRET=secret
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
## DHT
|
||||
## if you set this value, the DHT hyperswarm will start to announce and listen
|
||||
## on an hash created from this tpoic
|
||||
# DHT_TOPIC=GRADIDO_HUB
|
||||
# Federation
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
|
||||
# database
|
||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
379
docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md
Normal file
379
docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md
Normal file
@ -0,0 +1,379 @@
|
||||
# Zeitzonen
|
||||
|
||||
Die Gradido-Anwendung läuft im Backend in der Zeitzone UTC und im Frontend in der jeweiligen lokalen Zeitzone, in der der User sich anmeldet. Dadurch kann es zu zeitlichen Diskrepanzen kommen, die innerhalb der Anwendungslogik aufgelöst bzw. entsprechend behandelt werden müssen. In den folgenden Kapiteln werden die verschiedenen zeitlichen Konstellationen dargestellt und für die verschiedenen fachlichen Prozesse die daraus resultierenden Problemlösungen beschrieben.
|
||||
|
||||

|
||||
|
||||
## Beispiel 1
|
||||
|
||||
Ein User meldet sich in einer Zeitzone t0 - 4 an. Das bedeutet der User liegt 4 Stunden gegenüber der Backend-Zeit zurück.
|
||||
|
||||
Konkret hat der User die Zeit 31.08.2022 21:00:00 auf dem Server ist aber die Zeit bei 01.09.2022 01:00:00
|
||||
|
||||
Für die Erstellung einer Contribution hat der User noch folgende Gültigkeitsmonate und Beträge zur Wahl:
|
||||
|
||||
Juni 2022: 500 GDD | Juli 2022: 200 GDD | August 2022: 1000 GDD
|
||||
|
||||
**aber das Backend liefert nur die Beträge, die eigentlich so korrekt wären!!!!!**
|
||||
|
||||
**Juli 2022: 200 GDD | August 2022: 1000 GDD | September 2022: 1000 GDD**
|
||||
|
||||
Er möchte für den Juni 2022 eine Contribution mit 500 GDD erfassen. **Wird ihm der Juni noch als Schöpfungsmonat angezeigt?**
|
||||
|
||||
Falls ja, dann wählt er dabei im FE im Kalender den 30.06.2022. Dann liefert das FE folgende Contribution-Daten an das Backend:
|
||||
|
||||
* Gültigkeitsdatum: 30.06.2022 00:00:00
|
||||
* Memo: text
|
||||
* Betrag: 500 GDD
|
||||
* **Zeitzone: wird eine Zeitzone des User aus dem Context geliefert? Das fehlt: entweder über eine Zeit vom FE zum BE und ermitteln Offset im BE**
|
||||
|
||||
Im Backend wird dieses dann interpretiert und verarbeitet mit:
|
||||
|
||||
* **Belegung des Schöpfungsmonate-Arrays: [ 6, 7, 8] oder [7, 8, 9] da auf dem Server ja schon der 01.09.2022 ist?**
|
||||
* Gültigkeitsdatum: **30.06.2022 00:00:00 oder 01.07.2022 04:00:00 ?**
|
||||
* Memo: text
|
||||
* Betrag 500 GDD
|
||||
* created_at: 01.07.2022 04:00:00
|
||||
|
||||
**Frage: wird die Contribution dem Juni (6) oder dem Juli (7) zugeordnet?**
|
||||
|
||||
1. falls Juni zugeordnet kann die Contribution mit 500 GDD eingelöst werden
|
||||
2. falls Juli zugeordnet muss die Contribution mit 500 GDD abgelehnt werden, da möglicher Schöpfungsbetrag überschritten
|
||||
|
||||
|
||||
## Beispiel 2
|
||||
|
||||
|
||||
Ein User meldet sich in einer Zeitzone t0 + 1 an. Das bedeutet der User liegt 1 Stunde gegenüber der Backend-Zeit voraus.
|
||||
|
||||
Konkret hat der User die Zeit 01.09.2022 00:20:00 auf dem Server ist aber die Zeit bei 31.08.2022 23:20:00
|
||||
|
||||
Für die Erstellung einer Contribution hat der User noch folgende Gültigkeitsmonate und Beträge zur Wahl:
|
||||
|
||||
Juli 2022: 200 GDD | August 2022: 1000 GDD | September 2022: 1000 GDD
|
||||
|
||||
**oder wird ihm**
|
||||
|
||||
**
|
||||
Juni 2022: 500 GDD | Juli 2022: 200 GDD | August 2022: 1000 GDD**
|
||||
|
||||
**angezeigt, da auf dem BE noch der 31.08.2022 ist?**
|
||||
|
||||
Er möchte für den September 2022 eine Contribution mit 500 GDD erfassen und wählt dabei im FE im Kalender den 01.09.2022. Dann liefert das FE folgende Contribution-Daten an das Backend:
|
||||
|
||||
* Gültigkeitsdatum: 01.09.2022 00:00:00 (siehe Logauszüge der Fehleranalyse im Ticket #2179)
|
||||
* Memo: text
|
||||
* Betrag: 500 GDD
|
||||
* **Zeitzone: wird eine Zeitzone des User aus dem Context geliefert?**
|
||||
|
||||
Im Backend wird dieses dann interpretiert und verarbeitet mit:
|
||||
|
||||
* Belegung des Schöpfungsmonate-Arrays: [ 6, 7, 8] **wie kann der User dann aber vorher September 2022 für die Schöpfung auswählen?**
|
||||
* Gültigkeitsdatum: 01.09.2022 00:00:00
|
||||
* Memo: text
|
||||
* Betrag 500 GDD
|
||||
* created_at: 31.08.2022 23:20:00
|
||||
|
||||
Es kommt zu einem **Fehler im Backend**, da im Schöpfungsmonate-Array kein September (9) vorhanden ist, da auf dem Server noch der 31.08.2022 und damit das Array nur die Monate Juni, Juli, August und nicht September beinhaltet.
|
||||
|
||||
|
||||
## Erkenntnisse:
|
||||
|
||||
* die dem User angezeigten Schöpfungsmonate errechnen sich aus der lokalen User-Zeit und nicht aus der Backend-Zeit
|
||||
* das Backend muss somit für Ermittlung der möglichen Schöpfungsmonate und deren noch freien Schöpfungssummen den UserTimeOffset berücksichten
|
||||
* der gewählte Schöpfungsmonat muss 1:1 vom Frontend in das Backend übertragen werden
|
||||
* es darf kein Mapping in die Backend-Zeit erfolgen
|
||||
* sondern es muss der jeweilige UserTimeOffset mitgespeichert werden
|
||||
* die Logik im BE muss den übertragenen bzw. ermittelten Offset der FE-Zeit entsprechend berücksichten und nicht die Backendzeit in der Logik anwenden
|
||||
* im BE darf es kein einfaches now = new Date() geben
|
||||
* im BE muss stattdessen ein userNow = new Date() + UserTimeOffset verwendet werden
|
||||
* ein CreatedAt / UpdatedAt / DeletedAt / ConfirmedAt wird wie bisher in BE-Zeit gespeichert
|
||||
* **NEIN nicht notwendig:** plus in einer jeweils neuen Spalte CreatedOffset / UpdatedOffset / DeletedOffset / ConfirmedOffset der dabei gültige UserTimeOffset
|
||||
* im FE wird immer im Request-Header der aktuelle Zeitpunkt mit Zeitzone geschrieben
|
||||
*
|
||||
|
||||
## Entscheidung
|
||||
|
||||
* in den HTTP-Request-Header wird generell der aktuelle Timestamp des Clients eingetragen, sodass die aktuelle Uhrzeit des Users ohne weitere Signatur-Änderungen in jedem Aufruf am Backend ankommt. Moritz erstellt Ticket
|
||||
* es wird eine Analyse aller Backend-Aufrufe gemacht, die die Auswertung der User-Time und dessen evtl. Timezone-Differenz in der Logik des Backend-Aufrufs benötigt.
|
||||
* diese Backend-Methoden müssen fachlich so überarbeitet werden, dass immer aus dem Timezone-Offset die korrekte fachliche Logik als Ergebnis heraus kommt. In der Datenbank wird aber immer die UTC-Zeit gespeichert.
|
||||
* Es werden keine zusätzlichen Datanbank-Attribute zur Speicherung des User-TimeOffsets benötigt.
|
||||
|
||||
|
||||
## Analyse der Backend-Aufrufe
|
||||
|
||||
Es werden alle Resolver und ihre Methoden sowie im Resolver exportierte Attribute/Methoden untersucht.
|
||||
|
||||
Mit + gekennzeichnet sind diese, die mit dem UserTimeOffset interagieren und überarbeitet werden müssen.
|
||||
|
||||
Mit - gekennzeichnet sind diese, die keiner weiteren Aktion bedürfen.
|
||||
|
||||
|
||||
### AdminResolver
|
||||
|
||||
#### + adminCreateContribution
|
||||
|
||||
Hier wird der User zur übergebenen Email inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
|
||||
|
||||
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
|
||||
|
||||
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und der Initialisierung der Contribution berücksichtigt werden.
|
||||
|
||||
#### - adminCreateContributionMessage
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + adminCreateContributions
|
||||
|
||||
Hier wird eine Liste von übergebenen Contributions über den internen Aufruf von *adminCreateContribution()* verarbeitet. Da dort eine Berücksichtigung des User-TimeOffsets notwendig ist, muss hier die UserTime entsprechen im Context weitergereicht werden.
|
||||
|
||||
#### - adminDeleteContribution
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + adminUpdateContribution
|
||||
|
||||
analog adminCreateContribution() muss hier der User-TimeOffset berücksichtigt werden.
|
||||
|
||||
#### + confirmContribution
|
||||
|
||||
Hier wird intern *getUserCreation()* und *validateContribution()* aufgerufen, daher analog adminCreateContribution()
|
||||
|
||||
#### + createContributionLink
|
||||
|
||||
Hier werden zwar ein *ValidFrom* und ein *ValidTo* Datum übergeben, doch dürften diese keiner Beachtung eines User-TimezoneOffsets unterliegen. Trotzdem bitte noch einmal verifizieren.
|
||||
|
||||
#### - creationTransactionList
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - deleteContributionLink
|
||||
|
||||
Es wird zwar der *deletedAt*-Zeitpunkt als Rückgabewert geliefert, doch m.E. dürft hier keine Berücksichtigung des User-TimezoneOffsets notwendig sein.
|
||||
|
||||
#### - deleteUser
|
||||
|
||||
Es wird zwar der *deletedAt*-Zeitpunkt als Rückgabewert geliefert, doch m.E. dürft hier keine Berücksichtigung des User-TimezoneOffsets notwendig sein.
|
||||
|
||||
#### - listContributionLinks
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + listTransactionLinksAdmin
|
||||
|
||||
Hier wird die BE-Zeit für die Suche nach ValidUntil verwendet. Dies sollte nocheinmal verifiziert werden.
|
||||
|
||||
#### + listUnconfirmedContributions
|
||||
|
||||
Hier wird intern *getUserCreations()* aufgerufen für die Summen der drei Schöpfungsmonate, somit ist der User-TimezoneOffset zu berücksichtigen.
|
||||
|
||||
#### + searchUsers
|
||||
|
||||
Hier wird intern *getUserCreations()* aufgerufen für die Summen der drei Schöpfungsmonate, somit ist der User-TimezoneOffset zu berücksichtigen.
|
||||
|
||||
#### - sendActivationEmail
|
||||
|
||||
analog *UserResolver.checkOptInCode*
|
||||
|
||||
#### - setUserRole
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - unDeleteUser
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + updateContributionLink
|
||||
|
||||
Hier werden zwar ein *ValidFrom* und ein *ValidTo* Datum übergeben, doch dürften diese keiner Beachtung eines User-TimezoneOffsets unterliegen. Trotzdem bitte noch einmal verifizieren.
|
||||
|
||||
|
||||
### BalanceResolver
|
||||
|
||||
#### + balance
|
||||
|
||||
Hier wird der aktuelle Zeitpunkt des BE verwendet, um den Decay und die Summen der Kontostände zu ermitteln. Dies müsste eigentlich von dem User-TimezoneOffset unabhängig sein. Sollte aber noch einmal dahingehend verifiziert werden.
|
||||
|
||||
|
||||
### CommunityResolver
|
||||
|
||||
#### - communities
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - getCommunityInfo
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### ContributionMessageResolver
|
||||
|
||||
#### - createContributionMessage
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listContributionMessages
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### ContributionResolver
|
||||
|
||||
#### + createContribution
|
||||
|
||||
Hier wird der User inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
|
||||
|
||||
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
|
||||
|
||||
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und der Initialisierung der Contribution berücksichtigt werden.
|
||||
|
||||
#### - deleteContribution
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listAllContributions
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listContributions
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + updateContribution
|
||||
|
||||
Hier werden die Contributions des Users inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt.
|
||||
|
||||
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
|
||||
|
||||
Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und dem Update der Contribution berücksichtigt werden.
|
||||
|
||||
### GdtResolver
|
||||
|
||||
#### - existPid
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - gdtBalance
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listGDTEntries
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### KlicktippResolver
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### StatisticsResolver
|
||||
|
||||
#### + communityStatistics
|
||||
|
||||
Hier werden die Daten zum aktuellen BE-Zeitpunkt ermittelt und dem User angezeigt. Aber der User hat ggf. einen anderen TimeOffset. Daher die Frage, ob die Ermittlung der Statistik-Daten mit dem User-TimeOffset stattfinden muss.
|
||||
|
||||
|
||||
### TransactionLinkResolver
|
||||
|
||||
#### - transactionLinkCode
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - transactionLinkExpireDate
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - createTransactionLink
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - deleteTransactionLink
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - listTransactionLinks
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - queryTransactionLink
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - redeemTransactionLink
|
||||
|
||||
nothing to do
|
||||
|
||||
|
||||
### TransactionResolver
|
||||
|
||||
#### - executeTransaction
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - sendCoins
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + transactionList
|
||||
|
||||
Hier wird der aktuelle BE-Zeitpunkt verwendet, um die Summen der vorhandenen Transactions bis zu diesem Zeitpunkt zu ermitteln. Nach ersten Einschätzungen dürfte es hier nichts zu tun geben. Aber es sollte noch einmal geprüft werden.
|
||||
|
||||
|
||||
### UserResolver
|
||||
|
||||
#### - activationLink
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - checkOptInCode
|
||||
|
||||
Hier wird der übergebene OptIn-Code geprüft, ob schon wieder eine erneute Email gesendet werden kann. Die Zeiten werden auf reiner BE-Zeit verglichen, von daher gibt es hier nichts zu tun.
|
||||
|
||||
#### - createUser
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - forgotPassword
|
||||
|
||||
In dieser Methode wird am Ende in der Methode *sendResetPasswordEmailMailer()* die Zeit berechnet, wie lange der OptIn-Code im Link gültig ist, default 1440 min oder 24 h.
|
||||
|
||||
Es ist keine User-TimeOffset zu berücksichten, da der OptInCode direkt als Parameter im Aufruf von queryOptIn verwendet und dann dort mit der BE-Time verglichen wird.
|
||||
|
||||
#### - hasElopage
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - login
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - logout
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - queryOptIn
|
||||
|
||||
Hier wird der OptIn-Code aus der *sendResetPasswordEmailMailer()* als Parameter geliefert. Da dessen Gültigkeit zuvor in forgotPassword mit der BE-Zeit gesetzt wurde, benögt man hier keine Berücksichtigung des User-TimeOffsets.
|
||||
|
||||
#### - searchAdminUsers
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - setPassword
|
||||
|
||||
nothing to do, analog *queryOptIn*
|
||||
|
||||
#### - printTimeDuration
|
||||
|
||||
nothing to do
|
||||
|
||||
#### - updateUserInfos
|
||||
|
||||
nothing to do
|
||||
|
||||
#### + verifyLogin
|
||||
|
||||
Hier wird der User inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contribtutions, egal ob bestätigt oder noch offen ermittelt.
|
||||
|
||||
Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln.
|
||||
@ -0,0 +1,217 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="-PxXzgsMUT8aslXVdGG0" name="Seite-1">
|
||||
<mxGraphModel dx="1022" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="" style="endArrow=classic;html=1;strokeWidth=5;fillColor=#60a917;strokeColor=#2D7600;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="160" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="2160" y="320" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1160" y="360" as="sourcePoint"/>
|
||||
<mxPoint x="1160" y="320" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1130" y="370" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Backend<br style="font-size: 18px;">Zeitzone UTC" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="360" width="200" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="Frontend<br style="font-size: 18px;">verschiedene Zeitzonen" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="240" width="240" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1160" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1160" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1080" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1080" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1000" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1000" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="920" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="920" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="840" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="840" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="760" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="760" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="680" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="680" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="600" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="600" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="520" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="520" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="440" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="440" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="360" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="360" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="279.75" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="279.75" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="200" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="200" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="2120" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="2120" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="2040" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="2040" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1960" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1960" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1880" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1880" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1800" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1800" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1720" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1720" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1640" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1640" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1560" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1560" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1480" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1480" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1400" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1400" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1319.75" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1319.75" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="" style="endArrow=none;html=1;strokeWidth=5;" edge="1" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1240" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="1240" y="280" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="32" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1130" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="33" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 2</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="970" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="36" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 4</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="810" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 6</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="650" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="40" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 8</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="490" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="42" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px"> - 10</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="330" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="43" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 10</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1930" y="240" width="70" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="44" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 8</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1770" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="45" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 6</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1610" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="46" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 4</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1450" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="47" value="<font style="font-size: 24px"><b>t</b></font><font size="1"><b style="font-size: 13px">0</b><b style="font-size: 20px">&nbsp;+ 2</b></font>" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1290" y="240" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="89" value="mögliche Zeitzonen-Konstellationen" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=22;fontStyle=1" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="130" width="400" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
167
docu/RoadMap_2022-2023.md
Normal file
167
docu/RoadMap_2022-2023.md
Normal file
@ -0,0 +1,167 @@
|
||||
# Roadmap 2022 / 2023
|
||||
|
||||
## unsortierte Sammlung von Themen
|
||||
|
||||
1. backend access layer
|
||||
|
||||
- Refactoring der Resolver-Klassen
|
||||
- Daten-Zugriffschicht zur Kapselung der DB-Schicht
|
||||
- Transfer-Datenmodel zum Austausch von Daten zwischen den Schichten
|
||||
- technisches Transaktion-Handling und Lösung von Deadlocks
|
||||
- Konzept in Arbeit
|
||||
2. capturing alias
|
||||
|
||||
- Konzept fertig
|
||||
- Änderungen in Register- und Login-Prozess
|
||||
3. Passwort-Verschlüsselung: Refactoring
|
||||
|
||||
- Konzept aufteilen in Ausbaustufen
|
||||
- Altlasten entsorgen
|
||||
- Versionierung/Typisierung der verwendeten Verschlüsselungslogik notwendig
|
||||
- DB-Migration auf encryptionType=EMAIL
|
||||
4. Passwort-Verschlüsselung: Login mit impliziter Neuverschlüsselung
|
||||
|
||||
* Logik der Passwortverschlüsselung auf GradidoID einführen
|
||||
* bei Login mit encryptionType=Email oder OneTime triggern einer Neuverschlüsselung per GradidoID
|
||||
* Unabhängigkeit von Email erzeugen
|
||||
* Änderung der User-Email ermöglichen
|
||||
5. Contribution-Categories
|
||||
|
||||
- Bewertung und Kategorisierung von Schöpfungen: Was hat Wer für Wen geleistet?
|
||||
- Regeln auf Categories ermöglichen
|
||||
- Konzept in Arbeit
|
||||
6. Statistics / Analysen
|
||||
7. Contribution-Link editieren
|
||||
8. User-Tagging
|
||||
|
||||
- Eine UserTag dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User
|
||||
- Motivation des User-Taggings: bilden kleinerer lokaler User-Gruppen und jeder kennt jeden
|
||||
- Einführung einer UserTaggings-Tabelle und eine User-UserTaggings-Zuordnungs-Tabelle
|
||||
- Ein Moderator kann im AdminInterface die Liste der UserTags pflegen
|
||||
|
||||
- neues TAG anlegen
|
||||
- vorhandenes TAG umbenennen
|
||||
- ein TAG löschen, sofern kein User mehr diesem TAG zugeordnet ist
|
||||
- Will ein User ein TAG zugeordnet werden, so kann dies nur ein Moderator im AdminInterface tun
|
||||
- Ein Moderator kann im AdminInterface
|
||||
|
||||
- ein TAG einem User zuordnen
|
||||
- ein TAG von einem User entfernen
|
||||
- wichtige UseCases:
|
||||
|
||||
- Zuordnung eines Users zu einem TAG durch einen Moderator
|
||||
- TAG spezifische Schöpfung
|
||||
- User muss für seinen Beitrag ein TAG auswählen können, dem er zuvor zugeordnet wurde
|
||||
- TAG-Moderator kann den Beitrag bestätigen, weil er den User mit dem TAG (persönlich) kennt
|
||||
9. User-Beziehungen und Favoritenverwaltung
|
||||
|
||||
- User-User-Zuordnung
|
||||
- aus Tx-Liste die aktuellen Favoriten ermitteln
|
||||
- Verwaltung von Zuordnungen
|
||||
- Auswahl
|
||||
- Berechtigungen
|
||||
- Gruppierung
|
||||
- Community-übergreifend
|
||||
- User-Beziehungen
|
||||
10. technische Ablösung der Email und Ersatz durch GradidoID
|
||||
|
||||
* APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird
|
||||
* Email soll aber im Aussen für User optional noch verwendbar bleiben
|
||||
* Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird
|
||||
11. Zeitzone
|
||||
|
||||
- User sieht immer seine Locale-Zeit und Monate
|
||||
- Admin sieht immer UTC-Zeit und Monate
|
||||
- wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate)
|
||||
- Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)**
|
||||
- Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja
|
||||
- Beispiel: User in Tokyo Locale mit Offest +09:00
|
||||
|
||||
- aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung
|
||||
- die Contribution wird gespeichert mit
|
||||
|
||||
- creationDate=31.10.2022 22:00:00 UTC
|
||||
- contributionDate=01.11.2022 07:00:00
|
||||
- (neu) clientRequestTime=01.11.2022 07:00:00+09:00
|
||||
- durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit
|
||||
|
||||
- creationDate=31.10.2022 22:00:00 UTC
|
||||
- **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h**
|
||||
|
||||
- Prüfung auf Sommerzeiten und exotische Länder beachten
|
||||
-
|
||||
- zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern
|
||||
- Beispiel für täglichen Contribution-Link während des Monats:
|
||||
|
||||
- 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022
|
||||
- 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!!
|
||||
- Beispiel für täglichen Contribution-Link am Monatswechsel:
|
||||
|
||||
- 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022
|
||||
- 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!!
|
||||
12. Layout
|
||||
13. Lastschriften-Link
|
||||
14. Registrierung mit Redeem-Link:
|
||||
|
||||
* bei inaktivem Konto, sprich bisher noch keine Email-Bestätigung, keine Buchung möglich
|
||||
* somit speichern des Links zusammen mit OptIn-Code
|
||||
* damit kann in einem Resend der ConfirmationEmail der Link auch korrekt wieder mitgeliefert werden
|
||||
15. Manuelle User-Registrierung für Admin
|
||||
|
||||
- soll am 10.12.2022 für den Tag bei den Galliern produktiv sein
|
||||
16. Dezentralisierung / Federation
|
||||
|
||||
- Hyperswarm
|
||||
|
||||
- funktioniert schon im Prototyp
|
||||
- alle Instanzen finden sich gegenseitig
|
||||
- ToDo:
|
||||
- Infos aus HyperSwarm in der Community speichern
|
||||
- Prüfung ob neue mir noch unbekannte Community hinzugekommen ist?
|
||||
- Triggern der Authentifizierungs- und Autorisierungs-Handshake für neue Community
|
||||
- Authentifizierungs- und Autorisierungs-Handshake
|
||||
- Inter-Community-Communication
|
||||
- **ToDos**:
|
||||
|
||||
- DB-Migration für Community-Tabelle, User-Community-Zuordnungen, UserRights-Tabelle
|
||||
- Berechtigungen für Communities
|
||||
- Register- und Login-Prozess für Community-Anmeldung anpassen
|
||||
|
||||
- Auswahl-Box einer Community
|
||||
- createUser mit Zuordnung zur ausgewählten Community
|
||||
- Schöpfungsprozess auf angemeldete Community anpassen
|
||||
|
||||
- "Beitrag einreichen"-Dialog auf angemeldete Community anpassen
|
||||
- "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete Community anpassen
|
||||
- "Gemeinschaft"-Dialog auf angemeldete Community anpassen
|
||||
- "Mein Profil"-Dialog auf Communities anpassen
|
||||
|
||||
- Umzug-Service in andere Community
|
||||
- Löschen der Mitgliedschaft zu angemeldeter Community (Deaktivierung der Zuordnung "User-Community")
|
||||
- "Senden"-Dialog mit Community-Auswahl
|
||||
- "Transaktion"-Dialog mit Filter auf angemeldeter Community
|
||||
- AdminInterface auf angemeldete Community anpassen
|
||||
|
||||
- "Übersicht"-Dialog mit Filter auf angemeldete Community
|
||||
- "Nutzersuche"-Dialog mit Filter auf angemeldete Community
|
||||
- "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete Comunity
|
||||
- Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete Community anpassen
|
||||
|
||||
## Priorisierung
|
||||
|
||||
1. Contribution-Link editieren (vlt schon im vorherigen Bugfix-Release Ende Okt. 2022 fertig)
|
||||
2. Passwort-Verschlüsselung: Refactoring **Konzeption fertig!!**!
|
||||
3. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption ongoing!!**!
|
||||
4. Passwort-Verschlüsselung: implizite Login-Neuverschlüsselung **Konzeption fertig!!**!
|
||||
5. Layout
|
||||
6. Zeitzone
|
||||
7. Dezentralisierung / Federation
|
||||
8. capturing alias **Konzeption fertig!!**!
|
||||
9. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich
|
||||
10. Subgruppierung / User-Tagging (einfacher Ansatz)
|
||||
11. backend access layer
|
||||
12. technische Ablösung der Email und Ersatz durch GradidoID
|
||||
13. User-Beziehungen und Favoritenverwaltung
|
||||
14. Lastschriften-Link
|
||||
15. Contribution-Categories
|
||||
16. Statistics / Analysen
|
||||
60
docu/graphics/RoadMap2022-2023.drawio
Normal file
60
docu/graphics/RoadMap2022-2023.drawio
Normal file
@ -0,0 +1,60 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="CdUoMVivL2xThNJutTjM" 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="14" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" edge="1" parent="1" source="2" target="7">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="160" y="100"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="capturing alias" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="40" width="240" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Manuelle User-Registrierung für Admin (10.12.2022)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="200" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="Zeitzone" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="280" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="User-Beziehungen und Favoritenverwaltung" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="360" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="Layout" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="440" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" edge="1" parent="1" source="7" target="12">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="440" y="140"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Passwort-Verschlüsselung" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="80" width="240" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="Subgruppierung / Subcommunities" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="520" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="Contribution-Categories" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="600" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="backend access layer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="680" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="Statistics / Analysen" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="760" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="Ablösung der Email und Ersatz durch GradidoID" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="600" y="120" width="360" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="Dezentralisierung / Federation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;gradientColor=#b3b3b3;strokeColor=#666666;fontSize=16;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="840" width="440" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
BIN
docu/graphics/RoadMap2022-2023.png
Normal file
BIN
docu/graphics/RoadMap2022-2023.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bootstrap-vue-gradido-wallet",
|
||||
"version": "1.14.1",
|
||||
"version": "1.15.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node run/server.js",
|
||||
|
||||
@ -38,10 +38,12 @@ export default {
|
||||
form: {
|
||||
text: '',
|
||||
},
|
||||
isSubmitting: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.isSubmitting = true
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createContributionMessage,
|
||||
@ -55,9 +57,11 @@ export default {
|
||||
this.$emit('update-state', this.contributionId)
|
||||
this.form.text = ''
|
||||
this.toastSuccess(this.$t('message.reply'))
|
||||
this.isSubmitting = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
this.isSubmitting = false
|
||||
})
|
||||
},
|
||||
onReset() {
|
||||
@ -66,10 +70,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
disabled() {
|
||||
if (this.form.text !== '') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return this.form.text === '' || this.isSubmitting
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido",
|
||||
"version": "1.14.1",
|
||||
"version": "1.15.0",
|
||||
"description": "Gradido",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:gradido/gradido.git",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user