diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ce354b1e..26b71ea03 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/admin/package.json b/admin/package.json index 7f0e7ffd5..75800a526 100644 --- a/admin/package.json +++ b/admin/package.json @@ -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": { diff --git a/backend/.env.dist b/backend/.env.dist index 3b6fe2ce4..c0a2a6098 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -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 diff --git a/backend/.env.template b/backend/.env.template index d009d08ff..1bb2e4155 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -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 diff --git a/backend/package.json b/backend/package.json index 3e26225bf..519f9e6c0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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" diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e7139033b..c9e5ea79f 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -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 = { diff --git a/backend/src/federation/index.ts b/backend/src/federation/index.ts index 2ca58b432..82b961c63 100644 --- a/backend/src/federation/index.ts +++ b/backend/src/federation/index.ts @@ -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 => { 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 }) diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts new file mode 100644 index 000000000..b3a00d748 --- /dev/null +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -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 +}) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index a5c4a5f01..1b3558bb2 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -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 diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 9e74623c8..f4315d359 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -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,37 @@ 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(`user hasn't enough GDD or amount is < 0`)], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `user hasn't enough GDD or amount is < 0 : balance=null`, + ) + }) + }) + */ + describe('good transaction', () => { it('sends the coins', async () => { expect( diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index f0fb2f452..594039cfd 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -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) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 6323abfde..d8472fba9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -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', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 707b7ac49..752c585fd 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -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 { 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 } diff --git a/backend/src/index.ts b/backend/src/index.ts index dc1bbb115..e63f80827 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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, } } diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts new file mode 100644 index 000000000..971b6a32e --- /dev/null +++ b/backend/src/password/EncryptorUtils.ts @@ -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}`) + } +} diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts new file mode 100644 index 000000000..1735106c1 --- /dev/null +++ b/backend/src/password/PasswordEncryptor.ts @@ -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() +} diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index e885b7043..298348f0f 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -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.') }, diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 65214ebb5..9abb31554 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,17 +1,5 @@ -import Decimal from 'decimal.js-light' - export const objectValuesToArray = (obj: { [x: string]: string }): Array => { 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()) -} diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 9640cc614..edd8d55f6 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -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 } } diff --git a/database/Dockerfile b/database/Dockerfile index 4069ffcd8..03c7d9a3b 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -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 diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts new file mode 100644 index 000000000..2a3332925 --- /dev/null +++ b/database/entity/0053-change_password_encryption/User.ts @@ -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[] +} diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts new file mode 100644 index 000000000..97b12d4cd --- /dev/null +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -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 +} diff --git a/database/entity/User.ts b/database/entity/User.ts index d073f428a..b3c00a9b4 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0049-add_user_contacts_table/User' +export { User } from './0053-change_password_encryption/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index a368bb7ca..dd74e65c4 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0049-add_user_contacts_table/UserContact' +export { UserContact } from './0053-change_password_encryption/UserContact' diff --git a/database/log/.gitignore b/database/log/.gitignore new file mode 100644 index 000000000..c96a04f00 --- /dev/null +++ b/database/log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts new file mode 100644 index 000000000..635109430 --- /dev/null +++ b/database/migrations/0053-change_password_encryption.ts @@ -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>) { + 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>) { + 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;') +} diff --git a/database/migrations/0054-recalculate_balance_and_decay.ts b/database/migrations/0054-recalculate_balance_and_decay.ts new file mode 100644 index 000000000..516d0d1e3 --- /dev/null +++ b/database/migrations/0054-recalculate_balance_and_decay.ts @@ -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>) { + // 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>) {} diff --git a/database/package.json b/database/package.json index 6216a25fb..abc7789c4 100644 --- a/database/package.json +++ b/database/package.json @@ -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", diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 1d0e96455..9c6bfd735 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -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 diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md new file mode 100644 index 000000000..fa8573448 --- /dev/null +++ b/docu/RoadMap_2022-2023.md @@ -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 diff --git a/docu/graphics/RoadMap2022-2023.drawio b/docu/graphics/RoadMap2022-2023.drawio new file mode 100644 index 000000000..58b8dec94 --- /dev/null +++ b/docu/graphics/RoadMap2022-2023.drawio @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/graphics/RoadMap2022-2023.png b/docu/graphics/RoadMap2022-2023.png new file mode 100644 index 000000000..3ce8511a3 Binary files /dev/null and b/docu/graphics/RoadMap2022-2023.png differ diff --git a/frontend/package.json b/frontend/package.json index cfc12630e..6f1474521 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/package.json b/package.json index 72efee984..22f444155 100644 --- a/package.json +++ b/package.json @@ -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",