diff --git a/CHANGELOG.md b/CHANGELOG.md index a073aeb5f..6c64df990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ 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.6.6](https://github.com/gradido/gradido/compare/1.6.5...1.6.6) + +- Fix: Upper case email on register breaks account [`#1542`](https://github.com/gradido/gradido/pull/1542) +- 1106 first transaction cannot be expanded [`#1432`](https://github.com/gradido/gradido/pull/1432) +- added missing bootstrap scss. bootstrap/scss/bootstrap, plus more mis… [`#1540`](https://github.com/gradido/gradido/pull/1540) +- feat: Seed Deleted User [`#1533`](https://github.com/gradido/gradido/pull/1533) +- fix: No Creations for Deleted Users [`#1534`](https://github.com/gradido/gradido/pull/1534) +- fix: Wrong Key Name for Recover User [`#1535`](https://github.com/gradido/gradido/pull/1535) +- [Feature] : user deleted and undeleted functions for adminarea [`#1520`](https://github.com/gradido/gradido/pull/1520) +- fix: Possible SQL Exception in User Search [`#1530`](https://github.com/gradido/gradido/pull/1530) +- Feature: Make lint warnings unwanted [`#1529`](https://github.com/gradido/gradido/pull/1529) +- 1459 list data again on confirm creation [`#1467`](https://github.com/gradido/gradido/pull/1467) +- fix: Return Empty Array When No Pending Creations Are Present [`#1526`](https://github.com/gradido/gradido/pull/1526) +- Fix: Correct path of index.js in production [`#1525`](https://github.com/gradido/gradido/pull/1525) +- refactor: Get Open Creations by One Query [`#1524`](https://github.com/gradido/gradido/pull/1524) +- Admin: Langsame Benutzer-Suche [`#1472`](https://github.com/gradido/gradido/pull/1472) +- fix: Backend Unit Tests Running Again [`#1513`](https://github.com/gradido/gradido/pull/1513) +- Refactor: Combine transaction tables [`#1523`](https://github.com/gradido/gradido/pull/1523) +- Refactor: User resolver [`#1522`](https://github.com/gradido/gradido/pull/1522) +- feature: Soft-Delete for users (backend) [`#1521`](https://github.com/gradido/gradido/pull/1521) +- feature: Soft-Delete for users (database only) [`#1516`](https://github.com/gradido/gradido/pull/1516) +- refactor: Improve Decay Display [`#1517`](https://github.com/gradido/gradido/pull/1517) +- 404 page needs back to login button [`#1515`](https://github.com/gradido/gradido/pull/1515) +- feature: show current version in admin footer [`#1514`](https://github.com/gradido/gradido/pull/1514) +- fix: Never Sent Email Text [`#1512`](https://github.com/gradido/gradido/pull/1512) +- refactor: static decay block [`#1405`](https://github.com/gradido/gradido/pull/1405) +- refactor: Use Bootstrap Vue Toast [`#1499`](https://github.com/gradido/gradido/pull/1499) +- fix: Catch GDT Server Errors [`#1479`](https://github.com/gradido/gradido/pull/1479) +- Fix: Autochangelog - no commits [`#1498`](https://github.com/gradido/gradido/pull/1498) + #### [1.6.5](https://github.com/gradido/gradido/compare/1.6.4...1.6.5) > 15 February 2022 diff --git a/admin/package.json b/admin/package.json index 3cd0c54c0..0cbf57f5a 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.6.5", + "version": "1.6.6", "license": "MIT", "private": false, "scripts": { diff --git a/backend/package.json b/backend/package.json index b1a395e5f..1c611d32d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.6.5", + "version": "1.6.6", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 8961fc358..8869dd1aa 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -4,7 +4,7 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0024-combine_transaction_tables', + DB_VERSION: '0027-clean_transaction_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 } diff --git a/backend/src/graphql/enum/TransactionTypeId.ts b/backend/src/graphql/enum/TransactionTypeId.ts index 4ff3671cf..b5b75aa20 100644 --- a/backend/src/graphql/enum/TransactionTypeId.ts +++ b/backend/src/graphql/enum/TransactionTypeId.ts @@ -3,6 +3,7 @@ import { registerEnumType } from 'type-graphql' export enum TransactionTypeId { CREATION = 1, SEND = 2, + RECEIVE = 3, } registerEnumType(TransactionTypeId, { diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index 3aa3c429d..89ecd1513 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -15,6 +15,7 @@ export class Transaction { this.balance = 0 this.totalBalance = 0 this.memo = '' + this.firstTransaction = false } @Field(() => String) @@ -52,4 +53,7 @@ export class Transaction { @Field({ nullable: true }) decay?: Decay + + @Field(() => Boolean) + firstTransaction: boolean } diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 89b4d88b3..b6d8c38cd 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -20,8 +20,7 @@ import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs' import UpdatePendingCreationArgs from '../arg/UpdatePendingCreationArgs' import SearchUsersArgs from '../arg/SearchUsersArgs' import { Transaction } from '@entity/Transaction' -import { UserTransaction } from '@entity/UserTransaction' -import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction' +import { TransactionRepository } from '../../typeorm/repository/Transaction' import { calculateDecay } from '../../util/decay' import { AdminPendingCreation } from '@entity/AdminPendingCreation' import { hasElopageBuys } from '../../util/hasElopageBuys' @@ -306,42 +305,29 @@ export class AdminResolver { } const receivedCallDate = new Date() - let transaction = new Transaction() - transaction.transactionTypeId = TransactionTypeId.CREATION - transaction.memo = pendingCreation.memo - transaction.received = receivedCallDate - transaction.userId = pendingCreation.userId - transaction.amount = BigInt(parseInt(pendingCreation.amount.toString())) - transaction.creationDate = pendingCreation.date - transaction = await transaction.save() - if (!transaction) throw new Error('Could not create transaction') - const userTransactionRepository = getCustomRepository(UserTransactionRepository) - const lastUserTransaction = await userTransactionRepository.findLastForUser( - pendingCreation.userId, - ) + const transactionRepository = getCustomRepository(TransactionRepository) + const lastUserTransaction = await transactionRepository.findLastForUser(pendingCreation.userId) + let newBalance = 0 - if (!lastUserTransaction) { - newBalance = 0 - } else { + if (lastUserTransaction) { newBalance = calculateDecay( - lastUserTransaction.balance, + Number(lastUserTransaction.balance), lastUserTransaction.balanceDate, receivedCallDate, ).balance } newBalance = Number(newBalance) + Number(parseInt(pendingCreation.amount.toString())) - const newUserTransaction = new UserTransaction() - newUserTransaction.userId = pendingCreation.userId - newUserTransaction.transactionId = transaction.id - newUserTransaction.transactionTypeId = transaction.transactionTypeId - newUserTransaction.balance = Number(newBalance) - newUserTransaction.balanceDate = transaction.received - - await userTransactionRepository.save(newUserTransaction).catch((error) => { - throw new Error('Error saving user transaction: ' + error) - }) + const transaction = new Transaction() + transaction.typeId = TransactionTypeId.CREATION + transaction.memo = pendingCreation.memo + transaction.userId = pendingCreation.userId + transaction.amount = BigInt(parseInt(pendingCreation.amount.toString())) + transaction.creationDate = pendingCreation.date + transaction.balance = BigInt(newBalance) + transaction.balanceDate = receivedCallDate + await transaction.save() let userBalance = await Balance.findOne({ userId: pendingCreation.userId }) if (!userBalance) { @@ -388,7 +374,7 @@ async function getUserCreations(ids: number[], includePending = true): Promise= ${dateFilter} ${unionString}) AS result GROUP BY month, userId diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index d9a2e4974..be43a9b64 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' -import { getCustomRepository, getConnection, QueryRunner, In } from '@dbTools/typeorm' +import { getCustomRepository, getConnection, QueryRunner } from '@dbTools/typeorm' import CONFIG from '../../config' import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail' @@ -18,10 +18,9 @@ import Paginated from '../arg/Paginated' import { Order } from '../enum/Order' import { UserRepository } from '../../typeorm/repository/User' -import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction' +import { TransactionRepository } from '../../typeorm/repository/Transaction' import { User as dbUser } from '@entity/User' -import { UserTransaction as dbUserTransaction } from '@entity/UserTransaction' import { Transaction as dbTransaction } from '@entity/Transaction' import { Balance as dbBalance } from '@entity/Balance' @@ -33,166 +32,46 @@ import { TransactionType } from '../enum/TransactionType' import { hasUserAmount, isHexPublicKey } from '../../util/validate' import { RIGHTS } from '../../auth/RIGHTS' -// Helper function -async function calculateAndAddDecayTransactions( - userTransactions: dbUserTransaction[], - user: dbUser, - decay: boolean, - skipFirstTransaction: boolean, -): Promise { - const finalTransactions: Transaction[] = [] - const transactionIds: number[] = [] - const involvedUserIds: number[] = [] - - userTransactions.forEach((userTransaction: dbUserTransaction) => { - transactionIds.push(userTransaction.transactionId) - }) - - const transactions = await dbTransaction.find({ where: { id: In(transactionIds) } }) - const transactionIndiced: dbTransaction[] = [] - transactions.forEach((transaction: dbTransaction) => { - transactionIndiced[transaction.id] = transaction - involvedUserIds.push(transaction.userId) - if (transaction.transactionTypeId === TransactionTypeId.SEND) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - involvedUserIds.push(transaction.sendReceiverUserId!) // TODO ensure not null properly - } - }) - // remove duplicates - // https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates - const involvedUsersUnique = involvedUserIds.filter((v, i, a) => a.indexOf(v) === i) - const userRepository = getCustomRepository(UserRepository) - const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique) - - for (let i = 0; i < userTransactions.length; i++) { - const userTransaction = userTransactions[i] - const transaction = transactionIndiced[userTransaction.transactionId] - const finalTransaction = new Transaction() - finalTransaction.transactionId = transaction.id - finalTransaction.date = transaction.received.toISOString() - finalTransaction.memo = transaction.memo - finalTransaction.totalBalance = roundFloorFrom4(userTransaction.balance) - const previousTransaction = i > 0 ? userTransactions[i - 1] : null - - if (previousTransaction) { - const currentTransaction = userTransaction - const decay = calculateDecay( - previousTransaction.balance, - previousTransaction.balanceDate, - currentTransaction.balanceDate, - ) - const balance = previousTransaction.balance - decay.balance - - if (CONFIG.DECAY_START_TIME < currentTransaction.balanceDate) { - finalTransaction.decay = decay - finalTransaction.decay.balance = roundFloorFrom4(balance) - if ( - previousTransaction.balanceDate < CONFIG.DECAY_START_TIME && - currentTransaction.balanceDate > CONFIG.DECAY_START_TIME - ) { - finalTransaction.decay.decayStartBlock = ( - CONFIG.DECAY_START_TIME.getTime() / 1000 - ).toString() - } - } - } - - // sender or receiver when user has sent money - // group name if creation - // type: gesendet / empfangen / geschöpft - // transaktion nr / id - // date - // balance - if (userTransaction.transactionTypeId === TransactionTypeId.CREATION) { - // creation - finalTransaction.name = 'Gradido Akademie' - finalTransaction.type = TransactionType.CREATION - // finalTransaction.targetDate = creation.targetDate - finalTransaction.balance = roundFloorFrom4(Number(transaction.amount)) // Todo unsafe conversion - } else if (userTransaction.transactionTypeId === TransactionTypeId.SEND) { - // send coin - let otherUser: dbUser | undefined - finalTransaction.balance = roundFloorFrom4(Number(transaction.amount)) // Todo unsafe conversion - if (transaction.userId === user.id) { - finalTransaction.type = TransactionType.SEND - otherUser = userIndiced.find((u) => u.id === transaction.sendReceiverUserId) - // finalTransaction.pubkey = sendCoin.recipiantPublic - } else if (transaction.sendReceiverUserId === user.id) { - finalTransaction.type = TransactionType.RECIEVE - otherUser = userIndiced.find((u) => u.id === transaction.userId) - // finalTransaction.pubkey = sendCoin.senderPublic - } else { - throw new Error('invalid transaction') - } - if (otherUser) { - finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName - finalTransaction.email = otherUser.email - } - } - if (i > 0 || !skipFirstTransaction) { - finalTransactions.push(finalTransaction) - } - - if (i === userTransactions.length - 1 && decay) { - const now = new Date() - const decay = calculateDecay(userTransaction.balance, userTransaction.balanceDate, now) - const balance = userTransaction.balance - decay.balance - - const decayTransaction = new Transaction() - decayTransaction.type = 'decay' - decayTransaction.balance = roundCeilFrom4(balance) - decayTransaction.decayDuration = decay.decayDuration - decayTransaction.decayStart = decay.decayStart - decayTransaction.decayEnd = decay.decayEnd - finalTransactions.push(decayTransaction) - } - } - return finalTransactions -} - // helper helper function async function updateStateBalance( user: dbUser, - centAmount: number, + balance: number, received: Date, queryRunner: QueryRunner, ): Promise { - let balance = await dbBalance.findOne({ userId: user.id }) - if (!balance) { - balance = new dbBalance() - balance.userId = user.id - balance.amount = centAmount - balance.modified = received + let userBalance = await dbBalance.findOne({ userId: user.id }) + if (!userBalance) { + userBalance = new dbBalance() + userBalance.userId = user.id + userBalance.amount = balance + userBalance.modified = received } else { - const decayedBalance = calculateDecay(balance.amount, balance.recordDate, received).balance - balance.amount = Number(decayedBalance) + centAmount - balance.modified = new Date() + userBalance.amount = balance + userBalance.modified = new Date() } - if (balance.amount <= 0) { + if (userBalance.amount <= 0) { throw new Error('error new balance <= 0') } - balance.recordDate = received - return queryRunner.manager.save(balance).catch((error) => { + userBalance.recordDate = received + return queryRunner.manager.save(userBalance).catch((error) => { throw new Error('error saving balance:' + error) }) } -// helper helper function -async function addUserTransaction( - user: dbUser, - transaction: dbTransaction, +async function calculateNewBalance( + userId: number, + transactionDate: Date, centAmount: number, - queryRunner: QueryRunner, -): Promise { +): Promise { let newBalance = centAmount - const userTransactionRepository = getCustomRepository(UserTransactionRepository) - const lastUserTransaction = await userTransactionRepository.findLastForUser(user.id) + const transactionRepository = getCustomRepository(TransactionRepository) + const lastUserTransaction = await transactionRepository.findLastForUser(userId) if (lastUserTransaction) { newBalance += Number( calculateDecay( Number(lastUserTransaction.balance), lastUserTransaction.balanceDate, - transaction.received, + transactionDate, ).balance, ) } @@ -201,18 +80,8 @@ async function addUserTransaction( throw new Error('error new balance <= 0') } - const newUserTransaction = new dbUserTransaction() - newUserTransaction.userId = user.id - newUserTransaction.transactionId = transaction.id - newUserTransaction.transactionTypeId = transaction.transactionTypeId - newUserTransaction.balance = newBalance - newUserTransaction.balanceDate = transaction.received - - return queryRunner.manager.save(newUserTransaction).catch((error) => { - throw new Error('Error saving user transaction: ' + error) - }) + return newBalance } - @Resolver() export class TransactionResolver { @Authorized([RIGHTS.TRANSACTION_LIST]) @@ -243,22 +112,119 @@ export class TransactionResolver { if (offset && order === Order.ASC) { offset-- } - const userTransactionRepository = getCustomRepository(UserTransactionRepository) - const [userTransactions, userTransactionsCount] = - await userTransactionRepository.findByUserPaged(user.id, limit, offset, order, onlyCreations) + const transactionRepository = getCustomRepository(TransactionRepository) + const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged( + user.id, + limit, + offset, + order, + onlyCreations, + ) skipFirstTransaction = userTransactionsCount > offset + limit const decay = !(currentPage > 1) - let transactions: Transaction[] = [] + const transactions: Transaction[] = [] if (userTransactions.length) { if (order === Order.DESC) { userTransactions.reverse() } - transactions = await calculateAndAddDecayTransactions( - userTransactions, - user, - decay, - skipFirstTransaction, - ) + const involvedUserIds: number[] = [] + + userTransactions.forEach((transaction: dbTransaction) => { + involvedUserIds.push(transaction.userId) + if ( + transaction.typeId === TransactionTypeId.SEND || + transaction.typeId === TransactionTypeId.RECEIVE + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + involvedUserIds.push(transaction.linkedUserId!) // TODO ensure not null properly + } + }) + // remove duplicates + // https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates + const involvedUsersUnique = involvedUserIds.filter((v, i, a) => a.indexOf(v) === i) + const userRepository = getCustomRepository(UserRepository) + const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique) + + for (let i = 0; i < userTransactions.length; i++) { + const userTransaction = userTransactions[i] + const finalTransaction = new Transaction() + finalTransaction.transactionId = userTransaction.id + finalTransaction.date = userTransaction.balanceDate.toISOString() + finalTransaction.memo = userTransaction.memo + finalTransaction.totalBalance = roundFloorFrom4(Number(userTransaction.balance)) + const previousTransaction = i > 0 ? userTransactions[i - 1] : null + + if (previousTransaction) { + const currentTransaction = userTransaction + const decay = calculateDecay( + Number(previousTransaction.balance), + previousTransaction.balanceDate, + currentTransaction.balanceDate, + ) + const balance = Number(previousTransaction.balance) - decay.balance + + if (CONFIG.DECAY_START_TIME < currentTransaction.balanceDate) { + finalTransaction.decay = decay + finalTransaction.decay.balance = roundFloorFrom4(balance) + if ( + previousTransaction.balanceDate < CONFIG.DECAY_START_TIME && + currentTransaction.balanceDate > CONFIG.DECAY_START_TIME + ) { + finalTransaction.decay.decayStartBlock = ( + CONFIG.DECAY_START_TIME.getTime() / 1000 + ).toString() + } + } + } + + finalTransaction.balance = roundFloorFrom4(Number(userTransaction.amount)) // Todo unsafe conversion + + const otherUser = userIndiced.find((u) => u.id === userTransaction.linkedUserId) + switch (userTransaction.typeId) { + case TransactionTypeId.CREATION: + finalTransaction.name = 'Gradido Akademie' + finalTransaction.type = TransactionType.CREATION + break + case TransactionTypeId.SEND: + finalTransaction.type = TransactionType.SEND + if (otherUser) { + finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName + finalTransaction.email = otherUser.email + } + break + case TransactionTypeId.RECEIVE: + finalTransaction.type = TransactionType.RECIEVE + if (otherUser) { + finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName + finalTransaction.email = otherUser.email + } + break + default: + throw new Error('invalid transaction') + } + if (i > 0 || !skipFirstTransaction) { + transactions.push(finalTransaction) + } + + if (i === userTransactions.length - 1 && decay) { + const now = new Date() + const decay = calculateDecay( + Number(userTransaction.balance), + userTransaction.balanceDate, + now, + ) + const balance = Number(userTransaction.balance) - decay.balance + + const decayTransaction = new Transaction() + decayTransaction.type = 'decay' + decayTransaction.balance = roundCeilFrom4(balance) + decayTransaction.decayDuration = decay.decayDuration + decayTransaction.decayStart = decay.decayStart + decayTransaction.decayEnd = decay.decayEnd + transactions.push(decayTransaction) + } + } + if (order === Order.DESC) { transactions.reverse() } @@ -321,87 +287,62 @@ export class TransactionResolver { throw new Error('invalid recipient public key') } - const centAmount = Math.trunc(amount * 10000) + const centAmount = Math.round(amount * 10000) const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('READ UNCOMMITTED') try { + const receivedCallDate = new Date() // transaction - const transaction = new dbTransaction() - transaction.transactionTypeId = TransactionTypeId.SEND - transaction.memo = memo - transaction.userId = senderUser.id - transaction.pubkey = senderUser.pubKey - transaction.sendReceiverUserId = recipientUser.id - transaction.sendReceiverPublicKey = recipientUser.pubKey - transaction.amount = BigInt(centAmount) + const transactionSend = new dbTransaction() + transactionSend.typeId = TransactionTypeId.SEND + transactionSend.memo = memo + transactionSend.userId = senderUser.id + transactionSend.linkedUserId = recipientUser.id + transactionSend.amount = BigInt(centAmount) + const sendBalance = await calculateNewBalance(senderUser.id, receivedCallDate, -centAmount) + transactionSend.balance = BigInt(Math.trunc(sendBalance)) + transactionSend.balanceDate = receivedCallDate + transactionSend.sendSenderFinalBalance = transactionSend.balance + await queryRunner.manager.insert(dbTransaction, transactionSend) - await queryRunner.manager.insert(dbTransaction, transaction) - - // Insert Transaction: sender - amount - const senderUserTransactionBalance = await addUserTransaction( - senderUser, - transaction, - -centAmount, - queryRunner, - ) - - // Insert Transaction: recipient + amount - const recipiantUserTransactionBalance = await addUserTransaction( - recipientUser, - transaction, + const transactionReceive = new dbTransaction() + transactionReceive.typeId = TransactionTypeId.RECEIVE + transactionReceive.memo = memo + transactionReceive.userId = recipientUser.id + transactionReceive.linkedUserId = senderUser.id + transactionReceive.amount = BigInt(centAmount) + const receiveBalance = await calculateNewBalance( + recipientUser.id, + receivedCallDate, centAmount, - queryRunner, ) + transactionReceive.balance = BigInt(Math.trunc(receiveBalance)) + transactionReceive.balanceDate = receivedCallDate + transactionReceive.sendSenderFinalBalance = transactionSend.balance + transactionReceive.linkedTransactionId = transactionSend.id + await queryRunner.manager.insert(dbTransaction, transactionReceive) - // Update Balance: sender - amount - const senderStateBalance = await updateStateBalance( - senderUser, - -centAmount, - transaction.received, - queryRunner, - ) + // Save linked transaction id for send + transactionSend.linkedTransactionId = transactionReceive.id + await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) - // Update Balance: recipiant + amount - const recipiantStateBalance = await updateStateBalance( + // Update Balance sender + await updateStateBalance(senderUser, Math.trunc(sendBalance), receivedCallDate, queryRunner) + + // Update Balance recipient + await updateStateBalance( recipientUser, - centAmount, - transaction.received, + Math.trunc(receiveBalance), + receivedCallDate, queryRunner, ) - if (senderStateBalance.amount !== senderUserTransactionBalance.balance) { - throw new Error('db data corrupted, sender') - } - if (recipiantStateBalance.amount !== recipiantUserTransactionBalance.balance) { - throw new Error('db data corrupted, recipiant') - } - - // TODO: WTF? - // I just assume that due to implicit type conversion the decimal places were cut. - // Using `Math.trunc` to simulate this behaviour - transaction.sendSenderFinalBalance = BigInt(Math.trunc(senderStateBalance.amount)) - - await queryRunner.manager.save(transaction).catch((error) => { - throw new Error('error saving transaction with tx hash: ' + error) - }) - await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() - // TODO: This is broken code - we should never correct an autoincrement index in production - // according to dario it is required tho to properly work. The index of the table is used as - // index for the transaction which requires a chain without gaps - const count = await queryRunner.manager.count(dbTransaction) - // fix autoincrement value which seems not effected from rollback - await queryRunner - .query('ALTER TABLE `transactions` auto_increment = ?', [count]) - .catch((error) => { - // eslint-disable-next-line no-console - console.log('problems with reset auto increment: %o', error) - }) - throw e + throw new Error(`Transaction was not successful: ${e}`) } finally { await queryRunner.release() } diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index dfa685ed0..2612407bf 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -335,7 +335,7 @@ export class UserResolver { } // Validate email unique - // TODO: i can register an email in upper/lower case twice + email = email.trim().toLowerCase() // TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes const userFound = await DbUser.findOne({ email }, { withDeleted: true }) if (userFound) { @@ -408,6 +408,7 @@ export class UserResolver { @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) @Mutation(() => Boolean) async sendActivationEmail(@Arg('email') email: string): Promise { + email = email.trim().toLowerCase() const user = await DbUser.findOneOrFail({ email: email }) const queryRunner = getConnection().createQueryRunner() @@ -448,7 +449,7 @@ export class UserResolver { @Query(() => Boolean) async sendResetPasswordEmail(@Arg('email') email: string): Promise { // TODO: this has duplicate code with createUser - + email = email.trim().toLowerCase() const user = await DbUser.findOneOrFail({ email }) const optInCode = await getOptInCode(user.id) diff --git a/backend/src/typeorm/repository/UserTransaction.ts b/backend/src/typeorm/repository/Transaction.ts similarity index 76% rename from backend/src/typeorm/repository/UserTransaction.ts rename to backend/src/typeorm/repository/Transaction.ts index d699d07ea..2abcb4090 100644 --- a/backend/src/typeorm/repository/UserTransaction.ts +++ b/backend/src/typeorm/repository/Transaction.ts @@ -1,17 +1,17 @@ import { EntityRepository, Repository } from '@dbTools/typeorm' +import { Transaction } from '@entity/Transaction' import { Order } from '../../graphql/enum/Order' -import { UserTransaction } from '@entity/UserTransaction' import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId' -@EntityRepository(UserTransaction) -export class UserTransactionRepository extends Repository { +@EntityRepository(Transaction) +export class TransactionRepository extends Repository { findByUserPaged( userId: number, limit: number, offset: number, order: Order, onlyCreation?: boolean, - ): Promise<[UserTransaction[], number]> { + ): Promise<[Transaction[], number]> { if (onlyCreation) { return this.createQueryBuilder('userTransaction') .where('userTransaction.userId = :userId', { userId }) @@ -31,10 +31,10 @@ export class UserTransactionRepository extends Repository { .getManyAndCount() } - findLastForUser(userId: number): Promise { + findLastForUser(userId: number): Promise { return this.createQueryBuilder('userTransaction') .where('userTransaction.userId = :userId', { userId }) - .orderBy('userTransaction.transactionId', 'DESC') + .orderBy('userTransaction.balanceDate', 'DESC') .getOne() } } diff --git a/database/entity/0026-combine_transaction_tables2/Transaction.ts b/database/entity/0026-combine_transaction_tables2/Transaction.ts new file mode 100644 index 000000000..77f23e4e4 --- /dev/null +++ b/database/entity/0026-combine_transaction_tables2/Transaction.ts @@ -0,0 +1,83 @@ +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'user_id', unsigned: true, nullable: false }) + userId: number + + @Column({ name: 'transaction_id', unsigned: true, nullable: false }) + transactionId: number + + @Column({ name: 'transaction_type_id', unsigned: true, nullable: false }) + transactionTypeId: number + + @Column({ type: 'bigint', nullable: false }) + amount: BigInt + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ + name: 'send_sender_final_balance', + type: 'bigint', + nullable: true, + default: null, + }) + sendSenderFinalBalance: BigInt | null + + @Column({ name: 'balance', type: 'bigint', default: 0 }) + balance: BigInt + + @Column({ + name: 'balance_date', + type: 'timestamp', + default: () => 'CURRENT_TIMESTAMP', + nullable: false, + }) + balanceDate: Date + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + received: Date + + @Column({ name: 'creation_date', type: 'timestamp', nullable: true, default: null }) + creationDate: Date + + @Column({ + name: 'linked_user_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedUserId?: number | null + + @Column({ + name: 'linked_state_user_transaction_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedStateUserTransactionId?: number | null + + @Column({ type: 'binary', length: 64, nullable: true, default: null }) + signature: Buffer + + @Column({ name: 'tx_hash', type: 'binary', length: 48, default: null, nullable: true }) + txHash: Buffer + + @Column({ type: 'binary', length: 32, nullable: true, default: null }) + pubkey: Buffer + + @Column({ + name: 'creation_ident_hash', + type: 'binary', + length: 32, + nullable: true, + default: null, + }) + creationIdentHash: Buffer +} diff --git a/database/entity/0027-clean_transaction_table/Transaction.ts b/database/entity/0027-clean_transaction_table/Transaction.ts new file mode 100644 index 000000000..e50f5d164 --- /dev/null +++ b/database/entity/0027-clean_transaction_table/Transaction.ts @@ -0,0 +1,59 @@ +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'user_id', unsigned: true, nullable: false }) + userId: number + + @Column({ name: 'type_id', unsigned: true, nullable: false }) + typeId: number + + @Column({ type: 'bigint', nullable: false }) + amount: BigInt + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ + name: 'send_sender_final_balance', + type: 'bigint', + nullable: true, + default: null, + }) + sendSenderFinalBalance: BigInt | null + + @Column({ name: 'balance', type: 'bigint', default: 0 }) + balance: BigInt + + @Column({ + name: 'balance_date', + type: 'timestamp', + default: () => 'CURRENT_TIMESTAMP', + nullable: false, + }) + balanceDate: Date + + @Column({ name: 'creation_date', type: 'timestamp', nullable: true, default: null }) + creationDate: Date + + @Column({ + name: 'linked_user_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedUserId?: number | null + + @Column({ + name: 'linked_transaction_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedTransactionId?: number | null +} diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts index 5c9d2df88..af6a17424 100644 --- a/database/entity/Transaction.ts +++ b/database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0024-combine_transaction_tables/Transaction' +export { Transaction } from './0027-clean_transaction_table/Transaction' diff --git a/database/entity/UserTransaction.ts b/database/entity/UserTransaction.ts deleted file mode 100644 index bcbe6a65a..000000000 --- a/database/entity/UserTransaction.ts +++ /dev/null @@ -1 +0,0 @@ -export { UserTransaction } from './0001-init_db/UserTransaction' diff --git a/database/entity/index.ts b/database/entity/index.ts index 9371b5420..67f809b56 100644 --- a/database/entity/index.ts +++ b/database/entity/index.ts @@ -6,7 +6,6 @@ import { ServerUser } from './ServerUser' import { Transaction } from './Transaction' import { User } from './User' import { UserSetting } from './UserSetting' -import { UserTransaction } from './UserTransaction' import { AdminPendingCreation } from './AdminPendingCreation' export const entities = [ @@ -19,5 +18,4 @@ export const entities = [ Transaction, User, UserSetting, - UserTransaction, ] diff --git a/database/migrations/0024-combine_transaction_tables.ts b/database/migrations/0024-combine_transaction_tables.ts index b0595aa03..5b8ec8be8 100644 --- a/database/migrations/0024-combine_transaction_tables.ts +++ b/database/migrations/0024-combine_transaction_tables.ts @@ -1,6 +1,6 @@ -/* MIGRATION TO COMBINE ALL TRANSACTION TABLES +/* MIGRATION TO COMBINE SEVERAL TRANSACTION TABLES * - * Combine all transaction tables into one table with all data + * Combine several transaction tables into one table with all data */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ diff --git a/database/migrations/0025-emails_to_lower.ts b/database/migrations/0025-emails_to_lower.ts new file mode 100644 index 000000000..33f085e69 --- /dev/null +++ b/database/migrations/0025-emails_to_lower.ts @@ -0,0 +1,17 @@ +/* MIGRATION TO MAKE ALL EMAILS LOWERCASE + * + * Make all `email` values in `users` lowercase. + * This allows safe queries without any modificators + */ + +/* 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('UPDATE `users` SET `email` = LOWER(`email`);') +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // This migration cannot be revered +} diff --git a/database/migrations/0026-combine_transaction_tables2.ts b/database/migrations/0026-combine_transaction_tables2.ts new file mode 100644 index 000000000..3abf77354 --- /dev/null +++ b/database/migrations/0026-combine_transaction_tables2.ts @@ -0,0 +1,218 @@ +/* MIGRATION TO COMBINE AND REFACTOR SOME TRANSACTION TABLES + * + * Combine `state_user_transactions` and `transactions` tables. + * This changes the structure of transactions from 1 transaction for + * each send-coins to two transactions per send-coin + */ + +/* 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>) { + /* + * This migration has a possible incompatibility + * due to the construction of the tables. + * For our production data it works well. + * With this migration we decide for int instead of bigint + * to handle things more easily + * + * CREATE TABLE `transactions` ( + * `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + * ... + * ) + * CREATE TABLE `state_user_transactions` ( + * ... + * `transaction_id` int(10) unsigned NOT NULL, + * ... + * ) + */ + + // rename `state_user_id` to `user_id` + await queryFn('ALTER TABLE `state_user_transactions` RENAME COLUMN state_user_id TO user_id;') + // Create new `amount` column, with a temporary default of null + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `amount` bigint(20) DEFAULT NULL AFTER `transaction_type_id`;', + ) + // Create new `send_sender_final_balance` + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `send_sender_final_balance` bigint(20) DEFAULT NULL AFTER `amount`;', + ) + // Create new `memo` column, with a temporary default of null + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `memo` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL AFTER `amount`;', + ) + // Create new `received` column, with a temporary default of null + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `received` timestamp NULL DEFAULT NULL AFTER `balance_date`;', + ) + // Create new `creation_date` column + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `creation_date` timestamp NULL DEFAULT NULL AFTER `received`;', + ) + // Create new `linked_user_id` column (former `send_receiver_user_id`) + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `linked_user_id` int(10) unsigned DEFAULT NULL AFTER `creation_date`;', + ) + // Create new `linked_state_user_transaction_id` + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `linked_state_user_transaction_id` int(10) unsigned DEFAULT NULL AFTER `linked_user_id`;', + ) + // Create new `tx_hash` + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `tx_hash` binary(48) DEFAULT NULL AFTER `linked_state_user_transaction_id`;', + ) + // Create new `signature` + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `signature` binary(64) DEFAULT NULL AFTER `tx_hash`;', + ) + // Create new `pubkey` + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `pubkey` binary(32) DEFAULT NULL AFTER `signature`;', + ) + // Create new `creation_ident_hash` + await queryFn( + 'ALTER TABLE `state_user_transactions` ADD COLUMN `creation_ident_hash` binary(32) DEFAULT NULL AFTER `pubkey`;', + ) + + // Insert Data from `transactions` for creations + await queryFn(` + UPDATE state_user_transactions + LEFT JOIN transactions ON state_user_transactions.transaction_id = transactions.id + SET state_user_transactions.amount = transactions.amount, + state_user_transactions.send_sender_final_balance = transactions.send_sender_final_balance, + state_user_transactions.memo = transactions.memo, + state_user_transactions.received = transactions.received, + state_user_transactions.creation_date = transactions.creation_date, + state_user_transactions.linked_user_id = transactions.send_receiver_user_id, + state_user_transactions.linked_state_user_transaction_id = NULL, + state_user_transactions.tx_hash = transactions.tx_hash, + state_user_transactions.signature = transactions.signature, + state_user_transactions.pubkey = transactions.pubkey, + state_user_transactions.creation_ident_hash = transactions.creation_ident_hash + WHERE state_user_transactions.transaction_type_id = 1; + `) + + // Insert Data from `transactions` for sendCoin sender + await queryFn(` + UPDATE state_user_transactions + LEFT JOIN transactions ON state_user_transactions.transaction_id = transactions.id + SET state_user_transactions.amount = transactions.amount, + state_user_transactions.send_sender_final_balance = transactions.send_sender_final_balance, + state_user_transactions.memo = transactions.memo, + state_user_transactions.received = transactions.received, + state_user_transactions.creation_date = transactions.creation_date, + state_user_transactions.linked_user_id = transactions.send_receiver_user_id, + state_user_transactions.linked_state_user_transaction_id = ( + SELECT id FROM state_user_transactions AS sut + WHERE sut.transaction_type_id = 2 + AND sut.transaction_id = state_user_transactions.transaction_id + AND sut.user_id = transactions.send_receiver_user_id + ), + state_user_transactions.tx_hash = transactions.tx_hash, + state_user_transactions.signature = transactions.signature, + state_user_transactions.pubkey = transactions.pubkey, + state_user_transactions.creation_ident_hash = transactions.creation_ident_hash + WHERE state_user_transactions.transaction_type_id = 2 + AND state_user_transactions.user_id = transactions.user_id; + `) + + // Insert Data from `transactions` for sendCoin receiver + await queryFn(` + UPDATE state_user_transactions + LEFT JOIN transactions ON state_user_transactions.transaction_id = transactions.id + SET state_user_transactions.amount = transactions.amount, + state_user_transactions.send_sender_final_balance = transactions.send_sender_final_balance, + state_user_transactions.memo = transactions.memo, + state_user_transactions.received = transactions.received, + state_user_transactions.creation_date = transactions.creation_date, + state_user_transactions.linked_user_id = transactions.user_id, + state_user_transactions.linked_state_user_transaction_id = ( + SELECT id FROM state_user_transactions AS sut + WHERE sut.transaction_type_id = 2 + AND sut.transaction_id = state_user_transactions.transaction_id + AND sut.user_id = transactions.user_id + ), + state_user_transactions.tx_hash = transactions.tx_hash, + state_user_transactions.signature = transactions.signature, + state_user_transactions.pubkey = transactions.send_receiver_public_key, + state_user_transactions.creation_ident_hash = transactions.creation_ident_hash, + state_user_transactions.transaction_type_id = 3 + WHERE state_user_transactions.transaction_type_id = 2 + AND state_user_transactions.user_id = transactions.send_receiver_user_id; + `) + + // Modify defaults after our inserts + await queryFn('ALTER TABLE `state_user_transactions` MODIFY COLUMN `amount` bigint(20) NOT NULL;') + await queryFn( + 'ALTER TABLE `state_user_transactions` MODIFY COLUMN `memo` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL', + ) + await queryFn( + 'ALTER TABLE `state_user_transactions` MODIFY COLUMN `received` timestamp NOT NULL DEFAULT current_timestamp()', + ) + + // Drop table `transactions` + await queryFn('DROP TABLE `transactions`;') + + // Rename table `transaction_send_coins` to `transactions` + await queryFn('RENAME TABLE `state_user_transactions` TO `transactions`;') +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('RENAME TABLE `transactions` TO `state_user_transactions`;') + await queryFn(`CREATE TABLE \`transactions\` ( + \`id\` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + \`transaction_type_id\` int(10) unsigned NOT NULL, + \`user_id\` int(10) unsigned NOT NULL, + \`amount\` bigint(20) NOT NULL, + \`tx_hash\` binary(48) DEFAULT NULL, + \`memo\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + \`received\` timestamp NOT NULL DEFAULT current_timestamp(), + \`signature\` binary(64) DEFAULT NULL, + \`pubkey\` binary(32) DEFAULT NULL, + \`creation_ident_hash\` binary(32) DEFAULT NULL, + \`creation_date\` timestamp NULL DEFAULT NULL, + \`send_receiver_public_key\` binary(32) DEFAULT NULL, + \`send_receiver_user_id\` int(10) unsigned DEFAULT NULL, + \`send_sender_final_balance\` bigint(20) DEFAULT NULL, + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB AUTO_INCREMENT=3424 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `) + await queryFn(` + INSERT INTO transactions ( + id, transaction_type_id, user_id, amount, + tx_hash, memo, received, signature, pubkey, + creation_ident_hash, creation_date, + send_receiver_public_key, send_receiver_user_id, + send_sender_final_balance + ) + SELECT transaction_id AS id, transaction_type_id, + user_id, amount, tx_hash, memo, received, + signature, pubkey, creation_ident_hash, + creation_date, send_receiver_public_key, + linked_user_id AS send_receiver_user_id, + send_sender_final_balance + FROM state_user_transactions LEFT JOIN ( + SELECT id, pubkey AS send_receiver_public_key + FROM state_user_transactions AS sut + WHERE sut.transaction_type_id = 3 + ) AS sutj ON sutj.id = state_user_transactions.id + WHERE transaction_type_id IN (1,2) + `) + await queryFn( + 'UPDATE state_user_transactions SET transaction_type_id = 2 WHERE transaction_type_id = 3;', + ) + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `creation_ident_hash`;') + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `pubkey`;') + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `signature`;') + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `tx_hash`;') + await queryFn( + 'ALTER TABLE `state_user_transactions` DROP COLUMN `linked_state_user_transaction_id`;', + ) + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `linked_user_id`;') + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `creation_date`;') + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `received`;') + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `memo`;') + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `send_sender_final_balance`;') + await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `amount`;') + await queryFn('ALTER TABLE `state_user_transactions` RENAME COLUMN user_id TO state_user_id;') +} diff --git a/database/migrations/0027-clean_transaction_table.ts b/database/migrations/0027-clean_transaction_table.ts new file mode 100644 index 000000000..b5a0e0e2e --- /dev/null +++ b/database/migrations/0027-clean_transaction_table.ts @@ -0,0 +1,73 @@ +/* MIGRATION TO CLEAN THE TRANSACTION TABLE + * + * Remove several unused fields or those with duplicate data + * and rename fields to a proper name in `transactions` . + * + * This migration has data loss + */ + +/* 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>) { + // drop column `transaction_id`, it is not needed + await queryFn('ALTER TABLE `transactions` DROP COLUMN `transaction_id`;') + // drop column `received`, it is a duplicate of balance_date + await queryFn('ALTER TABLE `transactions` DROP COLUMN `received`;') + // drop column `tx_hash`, it is not needed + await queryFn('ALTER TABLE `transactions` DROP COLUMN `tx_hash`;') + // drop column `signature`, it is not needed + await queryFn('ALTER TABLE `transactions` DROP COLUMN `signature`;') + // drop column `pubkey`, it is not needed + await queryFn('ALTER TABLE `transactions` DROP COLUMN `pubkey`;') + // drop column `creation_ident_hash`, it is not needed + await queryFn('ALTER TABLE `transactions` DROP COLUMN `creation_ident_hash`;') + + // rename `transaction_type_id` to `type_id` + await queryFn('ALTER TABLE `transactions` RENAME COLUMN transaction_type_id TO type_id;') + // rename `linked_state_user_transaction_id` to `linked_transaction_id` + await queryFn( + 'ALTER TABLE `transactions` RENAME COLUMN linked_state_user_transaction_id TO linked_transaction_id;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // Not all data is recoverable here, some data is simulated, + // but we have data loss on: + // - transaction_id (we have data here, but its not the same as before) + // - tx_hash (null) + // - signature (null) + // - pubkey (null) + // - creation_ident_hash (null) + + await queryFn( + 'ALTER TABLE `transactions` RENAME COLUMN linked_transaction_id TO linked_state_user_transaction_id;', + ) + await queryFn('ALTER TABLE `transactions` RENAME COLUMN type_id TO transaction_type_id;') + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `creation_ident_hash` binary(32) DEFAULT NULL AFTER `linked_state_user_transaction_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `pubkey` binary(32) DEFAULT NULL AFTER `linked_state_user_transaction_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `signature` binary(64) DEFAULT NULL AFTER `linked_state_user_transaction_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `tx_hash` binary(48) DEFAULT NULL AFTER `linked_state_user_transaction_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `received` timestamp NULL DEFAULT NULL AFTER `balance_date`;', + ) + await queryFn('UPDATE `transactions` SET `received` = `balance_date`;') + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `received` timestamp NOT NULL DEFAULT current_timestamp();', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `transaction_id` int(10) unsigned DEFAULT NULL AFTER `user_id`;', + ) + await queryFn('UPDATE `transactions` SET `transaction_id` = `id`;') + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `transaction_id` int(10) unsigned NOT NULL;', + ) +} diff --git a/database/package.json b/database/package.json index 89fec74c9..a27e1fcbe 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.6.5", + "version": "1.6.6", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", diff --git a/database/src/factories/transaction.factory.ts b/database/src/factories/transaction.factory.ts index 2637ae298..9e8000223 100644 --- a/database/src/factories/transaction.factory.ts +++ b/database/src/factories/transaction.factory.ts @@ -2,7 +2,6 @@ import Faker from 'faker' import { define } from 'typeorm-seeding' import { Transaction } from '../../entity/Transaction' import { TransactionContext } from '../interface/TransactionContext' -import { randomBytes } from 'crypto' define(Transaction, (faker: typeof Faker, context?: TransactionContext) => { if (!context) { @@ -10,18 +9,13 @@ define(Transaction, (faker: typeof Faker, context?: TransactionContext) => { } const transaction = new Transaction() - transaction.transactionTypeId = context.transactionTypeId // || 2 + transaction.typeId = context.typeId // || 2 transaction.userId = context.userId transaction.amount = context.amount - transaction.txHash = context.txHash || randomBytes(48) transaction.memo = context.memo - transaction.received = context.received || new Date() - transaction.signature = context.signature || randomBytes(64) - transaction.pubkey = context.pubkey || randomBytes(32) - transaction.creationIdentHash = context.creationIdentHash || randomBytes(32) transaction.creationDate = context.creationDate || new Date() - transaction.sendReceiverPublicKey = context.sendReceiverPublicKey || null - transaction.sendReceiverUserId = context.sendReceiverUserId || null + // transaction.sendReceiverPublicKey = context.sendReceiverPublicKey || null + transaction.linkedUserId = context.sendReceiverUserId || null transaction.sendSenderFinalBalance = context.sendSenderFinalBalance || null return transaction diff --git a/database/src/factories/user-transaction.factory.ts b/database/src/factories/user-transaction.factory.ts deleted file mode 100644 index 7ea79235b..000000000 --- a/database/src/factories/user-transaction.factory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Faker from 'faker' -import { define } from 'typeorm-seeding' -import { UserTransaction } from '../../entity/UserTransaction' -import { UserTransactionContext } from '../interface/TransactionContext' - -define(UserTransaction, (faker: typeof Faker, context?: UserTransactionContext) => { - if (!context || !context.userId || !context.transactionId) { - throw new Error('UserTransaction: No userId and/or transactionId present!') - } - - const userTransaction = new UserTransaction() - userTransaction.userId = context.userId - userTransaction.transactionId = context.transactionId - userTransaction.transactionTypeId = context.transactionTypeId ? context.transactionTypeId : 1 - userTransaction.balance = context.balance ? context.balance : 100000 - userTransaction.balanceDate = context.balanceDate ? context.balanceDate : new Date() - - return userTransaction -}) diff --git a/database/src/interface/TransactionContext.ts b/database/src/interface/TransactionContext.ts index add9fc71c..9da737974 100644 --- a/database/src/interface/TransactionContext.ts +++ b/database/src/interface/TransactionContext.ts @@ -1,18 +1,13 @@ -import { Transaction } from '../../entity/Transaction' import { User } from '../../entity/User' export interface TransactionContext { - transactionTypeId: number + typeId: number userId: number + balance: BigInt + balanceDate: Date amount: BigInt - txHash?: Buffer memo: string - received?: Date - signature?: Buffer - pubkey?: Buffer - creationIdentHash?: Buffer creationDate?: Date - sendReceiverPublicKey?: Buffer sendReceiverUserId?: number sendSenderFinalBalance?: BigInt } @@ -23,23 +18,3 @@ export interface BalanceContext { amount?: number user?: User } - -export interface TransactionSendCoinContext { - senderPublic?: Buffer - userId?: number - recipiantPublic?: Buffer - recipiantUserId?: number - amount?: number - senderFinalBalance?: number - transaction?: Transaction -} - -export interface UserTransactionContext { - userId?: number - transactionId?: number - transactionTypeId?: number - balance?: number - balanceDate?: Date - signature?: Buffer - pubkey?: Buffer -} diff --git a/database/src/seeds/helpers/user-helpers.ts b/database/src/seeds/helpers/user-helpers.ts index a195fc181..fe43f857e 100644 --- a/database/src/seeds/helpers/user-helpers.ts +++ b/database/src/seeds/helpers/user-helpers.ts @@ -1,15 +1,10 @@ import { UserContext, ServerUserContext } from '../../interface/UserContext' -import { - BalanceContext, - TransactionContext, - UserTransactionContext, -} from '../../interface/TransactionContext' +import { BalanceContext, TransactionContext } from '../../interface/TransactionContext' import { UserInterface } from '../../interface/UserInterface' import { User } from '../../../entity/User' import { ServerUser } from '../../../entity/ServerUser' import { Balance } from '../../../entity/Balance' import { Transaction } from '../../../entity/Transaction' -import { UserTransaction } from '../../../entity/UserTransaction' import { Factory } from 'typeorm-seeding' export const userSeeder = async (factory: Factory, userData: UserInterface): Promise => { @@ -22,12 +17,9 @@ export const userSeeder = async (factory: Factory, userData: UserInterface): Pro if (userData.addBalance) { // create some GDD for the user await factory(Balance)(createBalanceContext(userData, user)).create() - const transaction = await factory(Transaction)( + await factory(Transaction)( createTransactionContext(userData, user, 1, 'Herzlich Willkommen bei Gradido!'), ).create() - await factory(UserTransaction)( - createUserTransactionContext(userData, user, transaction), - ).create() } } @@ -76,28 +68,12 @@ const createTransactionContext = ( memo: string, ): TransactionContext => { return { - transactionTypeId: type, + typeId: type, userId: user.id, amount: BigInt(context.amount || 100000), - txHash: context.creationTxHash, + balance: BigInt(context.amount || 100000), + balanceDate: new Date(context.recordDate || Date.now()), memo, - received: context.recordDate, creationDate: context.creationDate, } } - -const createUserTransactionContext = ( - context: UserInterface, - user: User, - transaction: Transaction, -): UserTransactionContext => { - return { - userId: user.id, - transactionId: transaction.id, - transactionTypeId: transaction.transactionTypeId, - balance: context.amount, - balanceDate: context.recordDate, - signature: context.signature, - pubkey: context.pubKey, - } -} diff --git a/frontend/package.json b/frontend/package.json index a46e50f85..5629e0360 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.6.5", + "version": "1.6.6", "private": true, "scripts": { "start": "node run/server.js", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 23d637de4..b14f5c8a2 100755 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -40,67 +40,3 @@ export default { }, } - diff --git a/frontend/src/assets/scss/gradido.scss b/frontend/src/assets/scss/gradido.scss index e29538807..9bc3231c4 100644 --- a/frontend/src/assets/scss/gradido.scss +++ b/frontend/src/assets/scss/gradido.scss @@ -3,15 +3,13 @@ @import "~bootstrap/scss/functions"; - @import "custom/variables"; // @import "~bootstrap/scss/variables"; wird am // ende der custom/variables angehangen - -// Bootstrap (4.5.3) mixins - -@import "~bootstrap/scss/mixins"; +// Bootstrap (4.5.3) mixins +@import "~bootstrap/scss/mixins"; + // Bootstrap (4.5.3) components @import "~bootstrap/scss/alert"; @@ -20,11 +18,18 @@ @import "~bootstrap/scss/button-group"; @import "~bootstrap/scss/buttons"; @import "~bootstrap/scss/card"; +@import "~bootstrap/scss/carousel"; +@import "~bootstrap/scss/close"; +@import "~bootstrap/scss/code"; @import "~bootstrap/scss/custom-forms"; +@import "~bootstrap/scss/dropdown"; @import "~bootstrap/scss/forms"; +@import "~bootstrap/scss/functions"; @import "~bootstrap/scss/grid"; @import "~bootstrap/scss/input-group"; @import "~bootstrap/scss/list-group"; +@import "~bootstrap/scss/mixins"; +@import "~bootstrap/scss/modal"; @import "~bootstrap/scss/nav"; @import "~bootstrap/scss/navbar"; @import "~bootstrap/scss/pagination"; @@ -38,7 +43,99 @@ @import "~bootstrap/scss/type"; @import "~bootstrap/scss/utilities"; @import "~bootstrap/scss/variables"; - +@import "~bootstrap/scss/bootstrap-grid"; +@import "~bootstrap/scss/bootstrap-reboot"; +@import "~bootstrap/scss/bootstrap"; // Bootstrap-vue (2.21.1) scss -@import '~bootstrap-vue/src/index.scss'; \ No newline at end of file +@import '~bootstrap-vue/src/index.scss'; + + +.alert-success { + color: #155724; + background-color: #d4edda; + border-color: #c3e6cb; +} +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + + +.b-toast-danger .toast .toast-header { + color: #721c24; + background-color: rgba(248,215,218,.85); + border-bottom-color: rgba(245,198,203,.85); +} +.b-toast-danger .toast .toast-body{ + background-color: rgba(252,237,238,.85); + border-color: rgba(245,198,203,.85); + color: #721c24; +} + +.b-toast-success .toast .toast-header { + color: #155724; + background-color: rgba(212,237,218,.58); + border-bottom-color: rgba(195,230,203,.85); +} +.b-toast-success .toast .toast-body{ + color: #155724; + background-color: rgba(212,237,218,.85); + border-bottom-color: rgba(195,230,203,.85); +} + + + .btn-primary pim { + background-color: #5a7b02; + border-color: #5e72e4; + } + a, + .copyright { + color: #5a7b02; + } + .font1_2em { + font-size: 1.2em; + } + .font2em { + font-size: 1.5em; + } + .gradido-global-color-text { + color: #3d443b; + } + .gradido-global-color-accent { + color: #047006; + } + .gradido-global-color-6e0a9c9e { + color: #000; + } + .gradido-global-color-2d0fb154 { + color: #047006; + } + .gradido-global-color-16efe88c { + color: #7ebc55; + } + .gradido-global-color-1939326 { + color: #f6fff6; + } + .gradido-global-color-9d79fc1 { + color: #047006; + } + .gradido-global-color-6347f4d { + color: #5a7b02; + } + .gradido-global-color-4fbc19a { + color: #014034; + } + .gradido-global-color-d341874 { + color: #b6d939; + } + .gradido-global-color-619d338 { + color: #8ebfb1; + } + .gradido-global-color-44819a9 { + color: #026873; + } + .gradido-global-color-gray { + color: #858383; + } \ No newline at end of file diff --git a/frontend/src/components/DecayInformation.vue b/frontend/src/components/DecayInformation.vue index 0daa45a18..9da5e4b2c 100644 --- a/frontend/src/components/DecayInformation.vue +++ b/frontend/src/components/DecayInformation.vue @@ -4,7 +4,7 @@ {{ decay ? ' − ' + $n(decay.balance, 'decimal') : '' }} -
+
@@ -56,17 +56,22 @@
{{ $t('decay.decay') }}
-
− {{ $n(decay.balance, 'decimal') }}
+
- {{ $n(decay.balance, 'decimal') }}
+
+ {{ $n(decay.balance + gddbalance, 'decimal') }} GDD - + {{ $n(decay.balance, 'decimal') }} GDD = + {{ $n(gddbalance, 'decimal') }} GDD +

- + {{ $t('decay.calculation_total') }} - +
{{ $t('decay.sent') }}
{{ $t('decay.received') }}
@@ -77,7 +82,7 @@
- +
{{ $t('decay.decay') }}
@@ -86,7 +91,7 @@
- +
{{ $t('decay.total') }}
@@ -109,6 +114,7 @@ export default { name: 'DecayInformation', props: { + gddbalance: { type: Number }, balance: { type: Number }, type: { type: String, default: '' }, decay: { diff --git a/frontend/src/components/Transaction.spec.js b/frontend/src/components/Transaction.spec.js index aaad023e0..ac80fefc8 100644 --- a/frontend/src/components/Transaction.spec.js +++ b/frontend/src/components/Transaction.spec.js @@ -31,8 +31,13 @@ describe('Transaction', () => { expect(wrapper.find('div.gdt-transaction-list-item').exists()).toBeTruthy() }) - it('has a collapse button', () => { - expect(wrapper.find('button[type="button"].btn-secondary').text()).toBe('i') + it('has a collapse icon bi-caret-down-square', () => { + expect(wrapper.find('div.gdt-transaction-list-item').findAll('svg').at(1).classes()).toEqual([ + 'bi-caret-down-square', + 'b-icon', + 'bi', + 'text-muted', + ]) }) describe('no valid GDT entry type', () => { diff --git a/frontend/src/components/Transaction.vue b/frontend/src/components/Transaction.vue index 1dcfee13a..649b81b4d 100644 --- a/frontend/src/components/Transaction.vue +++ b/frontend/src/components/Transaction.vue @@ -9,9 +9,10 @@
- - i - +
@@ -85,6 +86,16 @@ export default { gdt: { type: Number }, id: { type: Number }, }, + data() { + return { + collapseStatus: [], + } + }, + methods: { + getCollapseState(id) { + return this.collapseStatus.includes('gdt-collapse-' + id) + }, + }, computed: { collapseId() { return 'gdt-collapse-' + String(this.id) @@ -130,5 +141,14 @@ export default { } }, }, + mounted() { + this.$root.$on('bv::collapse::state', (collapseId, isJustShown) => { + if (isJustShown) { + this.collapseStatus.push(collapseId) + } else { + this.collapseStatus = this.collapseStatus.filter((id) => id !== collapseId) + } + }) + }, } diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index ff211fc07..70d0ea5a4 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -78,6 +78,7 @@ export const transactionsQuery = gql` decayDuration decayStartBlock } + firstTransaction } } } diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 0fdac973f..3bfad63e8 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -21,6 +21,7 @@ "switch-to-this-community": "zu dieser Gemeinschaft wechseln" }, "decay": { + "befor_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.", "calculation_decay": "Berechnung der Vergänglichkeit", "calculation_total": "Berechnung der Gesamtsumme", "created": "Geschöpft", @@ -29,6 +30,7 @@ "decayStart": " - Startblock für Vergänglichkeit am: ", "decay_introduced": "Die Vergänglichkeit wurde eingeführt am ", "decay_since_last_transaction": "Vergänglichkeit seit der letzten Transaktion", + "first_transaction": "Die erste Transaktion beinhaltet keine Vergänglichkeit.", "hours": "Stunden", "last_transaction": "Letzte Transaktion", "minutes": "Minuten", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index fdbf37bd7..b06030947 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -21,6 +21,7 @@ "switch-to-this-community": "Switch to this community" }, "decay": { + "befor_startblock_transaction": "This transaction does not include decay.", "calculation_decay": "Calculation of Decay", "calculation_total": "Calculation of the total Amount", "created": "Created", @@ -29,6 +30,7 @@ "decayStart": " - Starting block for decay at: ", "decay_introduced": "Decay was introduced on", "decay_since_last_transaction": "Decay since the last transaction", + "first_transaction": "The first transaction does not include decay.", "hours": "Hours", "last_transaction": "Last transaction:", "minutes": "Minutes", diff --git a/frontend/src/mixins/toaster.js b/frontend/src/mixins/toaster.js index 704eaf1a7..4464a2cc9 100644 --- a/frontend/src/mixins/toaster.js +++ b/frontend/src/mixins/toaster.js @@ -16,7 +16,7 @@ export const toasters = { message = message.replace(/^GraphQL error: /, '') this.$bvToast.toast(message, { autoHideDelay: 5000, - appendToast: false, + appendToast: true, solid: true, toaster: 'b-toaster-top-right', headerClass: 'gdd-toaster-title', diff --git a/frontend/src/views/Pages/AccountOverview.vue b/frontend/src/views/Pages/AccountOverview.vue index b62481806..c0ba55155 100644 --- a/frontend/src/views/Pages/AccountOverview.vue +++ b/frontend/src/views/Pages/AccountOverview.vue @@ -15,6 +15,7 @@
{ decay: { balance: '1.5' }, }, { - balance: '1.07', + balance: 1.07, type: 'decay', }, ], @@ -135,12 +135,31 @@ describe('GddTransactionList', () => { transaction = wrapper.findAll('div.gdd-transaction-list-item').at(0) }) + it('has a bi-caret-down-square icon', () => { + expect(transaction.findAll('svg').at(0).classes()).toEqual([ + 'bi-caret-down-square', + 'b-icon', + 'bi', + 'text-muted', + ]) + }) + + // it('transaction is clicked', async () => { + // await transaction.trigger('click') + // expect(transaction.findAll('svg').at(0).classes()).toEqual([ + // 'bi-caret-up-square', + // 'b-icon', + // 'bi', + // 'text-muted', + // ]) + // }) + it('has a bi-arrow-left-circle icon', () => { - expect(transaction.find('svg').classes()).toContain('bi-arrow-left-circle') + expect(transaction.findAll('svg').at(1).classes()).toContain('bi-arrow-left-circle') }) it('has text-danger color', () => { - expect(transaction.find('svg').classes()).toContain('text-danger') + expect(transaction.findAll('svg').at(1).classes()).toContain('text-danger') }) it('has a minus operator', () => { @@ -186,12 +205,33 @@ describe('GddTransactionList', () => { transaction = wrapper.findAll('div.gdd-transaction-list-item').at(1) }) + it('has a bi-caret-down-square icon', () => { + expect(transaction.findAll('svg').at(0).classes()).toEqual([ + 'bi-caret-down-square', + 'b-icon', + 'bi', + 'text-muted', + ]) + }) + + // it('transaction is clicked', async () => { + // await transaction.trigger('click') + // expect(transaction.findAll('svg').at(0).classes()).toEqual([ + // 'bi-caret-up-square', + // 'b-icon', + // 'bi', + // 'text-muted', + // ]) + // }) + it('has a bi-gift icon', () => { - expect(transaction.find('svg').classes()).toContain('bi-gift') + expect(transaction.findAll('svg').at(1).classes()).toContain('bi-gift') }) it('has gradido-global-color-accent color', () => { - expect(transaction.find('svg').classes()).toContain('gradido-global-color-accent') + expect(transaction.findAll('svg').at(1).classes()).toContain( + 'gradido-global-color-accent', + ) }) it('has a plus operator', () => { @@ -225,12 +265,33 @@ describe('GddTransactionList', () => { transaction = wrapper.findAll('div.gdd-transaction-list-item').at(2) }) + it('has a bi-caret-down-square icon', () => { + expect(transaction.findAll('svg').at(0).classes()).toEqual([ + 'bi-caret-down-square', + 'b-icon', + 'bi', + 'text-muted', + ]) + }) + + // it('transaction is clicked', async () => { + // await transaction.trigger('click') + // expect(transaction.findAll('svg').at(0).classes()).toEqual([ + // 'bi-caret-up-square', + // 'b-icon', + // 'bi', + // 'text-muted', + // ]) + // }) + it('has a bi-arrow-right-circle icon', () => { - expect(transaction.find('svg').classes()).toContain('bi-arrow-right-circle') + expect(transaction.findAll('svg').at(1).classes()).toContain('bi-arrow-right-circle') }) it('has gradido-global-color-accent color', () => { - expect(transaction.find('svg').classes()).toContain('gradido-global-color-accent') + expect(transaction.findAll('svg').at(1).classes()).toContain( + 'gradido-global-color-accent', + ) }) it('has a plus operator', () => { @@ -276,12 +337,31 @@ describe('GddTransactionList', () => { transaction = wrapper.findAll('div.gdd-transaction-list-item').at(3) }) + it('has a bi-caret-down-square icon', () => { + expect(transaction.findAll('svg').at(0).classes()).toEqual([ + 'bi-caret-down-square', + 'b-icon', + 'bi', + 'text-muted', + ]) + }) + + // it('transaction is clicked', async () => { + // await transaction.trigger('click') + // expect(transaction.findAll('svg').at(0).classes()).toEqual([ + // 'bi-caret-up-square', + // 'b-icon', + // 'bi', + // 'text-muted', + // ]) + // }) + it('has a bi-droplet-half icon', () => { - expect(transaction.find('svg').classes()).toContain('bi-droplet-half') + expect(transaction.findAll('svg').at(1).classes()).toContain('bi-droplet-half') }) it('has gradido-global-color-gray color', () => { - expect(transaction.find('svg').classes()).toContain('gradido-global-color-gray') + expect(transaction.findAll('svg').at(1).classes()).toContain('gradido-global-color-gray') }) it('has a minus operator', () => { diff --git a/frontend/src/views/Pages/AccountOverview/GddTransactionList.vue b/frontend/src/views/Pages/AccountOverview/GddTransactionList.vue index 4e2df58d1..db24e90ba 100644 --- a/frontend/src/views/Pages/AccountOverview/GddTransactionList.vue +++ b/frontend/src/views/Pages/AccountOverview/GddTransactionList.vue @@ -2,35 +2,44 @@
- + {{ $t('error.no-transactionlist') }}
- + {{ $t('error.empty-transactionlist') }}
-
- - i - +
+
-
@@ -112,9 +121,13 @@
- - -
+ + +
+
+ {{ $t('decay.first_transaction') }} +
+ +
+ {{ $t('decay.befor_startblock_transaction') }} +
+
+ +
@@ -161,9 +198,11 @@ export default { data() { return { currentPage: 1, + collapseStatus: [], } }, props: { + gddbalance: { type: Number }, transactions: { default: () => [] }, pageSize: { type: Number, default: 25 }, timestamp: { type: Number, default: 0 }, @@ -191,6 +230,18 @@ export default { throwError(msg) { throw new Error(msg) }, + getCollapseState(transactionId) { + return this.collapseStatus.includes('decay-' + transactionId) + }, + }, + mounted() { + this.$root.$on('bv::collapse::state', (collapseId, isJustShown) => { + if (isJustShown) { + this.collapseStatus.push(collapseId) + } else { + this.collapseStatus = this.collapseStatus.filter((id) => id !== collapseId) + } + }) }, watch: { currentPage() { diff --git a/frontend/src/views/Pages/Register.vue b/frontend/src/views/Pages/Register.vue index 8d95c8197..03012cf34 100755 --- a/frontend/src/views/Pages/Register.vue +++ b/frontend/src/views/Pages/Register.vue @@ -108,7 +108,7 @@ v-if="showError" show dismissible - variant="warning" + variant="danger" @dismissed="closeAlert" > diff --git a/frontend/src/views/Pages/UserProfileTransactionList.vue b/frontend/src/views/Pages/UserProfileTransactionList.vue index 383e908b6..494819f05 100644 --- a/frontend/src/views/Pages/UserProfileTransactionList.vue +++ b/frontend/src/views/Pages/UserProfileTransactionList.vue @@ -5,6 +5,7 @@

{{ $t('transaction.gdd-text') }}