Merge branch 'user_soft_delete_backend' into test-softdeleted

This commit is contained in:
ogerly 2022-02-18 13:21:05 +01:00 committed by Ulf Gebhardt
commit 0de018b296
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
40 changed files with 551 additions and 566 deletions

View File

@ -431,7 +431,7 @@ jobs:
unit_test_backend: unit_test_backend:
name: Unit tests - Backend name: Unit tests - Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_backend,build_test_mariadb] needs: [build_test_mariadb]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -448,13 +448,6 @@ jobs:
path: /tmp path: /tmp
- name: Load Docker Image - name: Load Docker Image
run: docker load < /tmp/mariadb.tar run: docker load < /tmp/mariadb.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v2
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
########################################################################## ##########################################################################
# UNIT TESTS BACKEND ##################################################### # UNIT TESTS BACKEND #####################################################
########################################################################## ##########################################################################
@ -469,7 +462,7 @@ jobs:
run: sleep 30s run: sleep 30s
shell: bash shell: bash
- name: backend Unit tests | test - name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn CI_workflow_test run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test # run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
########################################################################## ##########################################################################
# COVERAGE CHECK BACKEND ################################################# # COVERAGE CHECK BACKEND #################################################
@ -480,7 +473,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 38 min_coverage: 48
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################

View File

@ -1,21 +1,18 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = async () => { module.exports = {
process.env.TZ = 'UTC' verbose: true,
return { preset: 'ts-jest',
verbose: true, collectCoverage: true,
preset: 'ts-jest', collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
collectCoverage: true, setupFiles: ['<rootDir>/test/testSetup.ts'],
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'], moduleNameMapper: {
moduleNameMapper: { '@entity/(.*)':
'@entity/(.*)': '<rootDir>/../database/build/entity/$1', process.env.NODE_ENV === 'development'
// This is hack to fix a problem with the library `ts-mysql-migrate` which does differentiate between its ts/js state ? '<rootDir>/../database/entity/$1'
'@dbTools/(.*)': '<rootDir>/../database/src/$1', : '<rootDir>/../database/build/entity/$1',
/* '@dbTools/(.*)':
'@dbTools/(.*)': process.env.NODE_ENV === 'development'
process.env.NODE_ENV === 'development' ? '<rootDir>/../database/src/$1'
? '<rootDir>/../database/src/$1' : '<rootDir>/../database/build/src/$1',
: '<rootDir>/../database/build/src/$1', },
*/
},
}
} }

View File

@ -13,8 +13,7 @@
"start": "node build/index.js", "start": "node build/index.js",
"dev": "nodemon -w src --ext ts --exec ts-node src/index.ts", "dev": "nodemon -w src --ext ts --exec ts-node src/index.ts",
"lint": "eslint . --ext .js,.ts", "lint": "eslint . --ext .js,.ts",
"CI_workflow_test": "jest --runInBand --coverage ", "test": "TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles"
"test": "NODE_ENV=development jest --runInBand --coverage "
}, },
"dependencies": { "dependencies": {
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",

View File

@ -4,7 +4,7 @@ import dotenv from 'dotenv'
dotenv.config() dotenv.config()
const constants = { const constants = {
DB_VERSION: '0023-users_disabled_soft_delete', DB_VERSION: '0024-combine_transaction_tables',
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
} }

View File

@ -2,27 +2,27 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx } from 'type-graphql' import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx } from 'type-graphql'
import { getCustomRepository, Raw } from '@dbTools/typeorm' import { getCustomRepository, ObjectLiteral, Raw } from '@dbTools/typeorm'
import { UserAdmin, SearchUsersResult } from '../model/UserAdmin' import { UserAdmin, SearchUsersResult } from '../model/UserAdmin'
import { PendingCreation } from '../model/PendingCreation' import { PendingCreation } from '../model/PendingCreation'
import { CreatePendingCreations } from '../model/CreatePendingCreations' import { CreatePendingCreations } from '../model/CreatePendingCreations'
import { UpdatePendingCreation } from '../model/UpdatePendingCreation' import { UpdatePendingCreation } from '../model/UpdatePendingCreation'
import { RIGHTS } from '../../auth/RIGHTS' import { RIGHTS } from '../../auth/RIGHTS'
import { TransactionRepository } from '../../typeorm/repository/Transaction'
import { UserRepository } from '../../typeorm/repository/User' import { UserRepository } from '../../typeorm/repository/User'
import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs' import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs'
import UpdatePendingCreationArgs from '../arg/UpdatePendingCreationArgs' import UpdatePendingCreationArgs from '../arg/UpdatePendingCreationArgs'
import SearchUsersArgs from '../arg/SearchUsersArgs' import SearchUsersArgs from '../arg/SearchUsersArgs'
import moment from 'moment' import moment from 'moment'
import { Transaction } from '@entity/Transaction' import { Transaction } from '@entity/Transaction'
import { TransactionCreation } from '@entity/TransactionCreation'
import { UserTransaction } from '@entity/UserTransaction' import { UserTransaction } from '@entity/UserTransaction'
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction' import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
import { BalanceRepository } from '../../typeorm/repository/Balance'
import { calculateDecay } from '../../util/decay' import { calculateDecay } from '../../util/decay'
import { AdminPendingCreation } from '@entity/AdminPendingCreation' import { AdminPendingCreation } from '@entity/AdminPendingCreation'
import { hasElopageBuys } from '../../util/hasElopageBuys' import { hasElopageBuys } from '../../util/hasElopageBuys'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { User } from '@entity/User'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { Balance } from '@entity/Balance'
// const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_IN_REGISTER = 1
// const EMAIL_OPT_UNKNOWN = 3 // elopage? // const EMAIL_OPT_UNKNOWN = 3 // elopage?
@ -35,8 +35,26 @@ export class AdminResolver {
@Args() { searchText, currentPage = 1, pageSize = 25, notActivated = false }: SearchUsersArgs, @Args() { searchText, currentPage = 1, pageSize = 25, notActivated = false }: SearchUsersArgs,
): Promise<SearchUsersResult> { ): Promise<SearchUsersResult> {
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const users = await userRepository.findBySearchCriteria(searchText)
let adminUsers = await Promise.all( const filterCriteria: ObjectLiteral[] = []
if (notActivated) {
filterCriteria.push({ emailChecked: false })
}
// prevent overfetching data from db, select only needed columns
// prevent reading and transmitting data from db at least 300 Bytes
// one of my example dataset shrink down from 342 Bytes to 42 Bytes, that's ~88% saved db bandwith
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked']
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
userFields.map((fieldName) => {
return 'user.' + fieldName
}),
searchText,
filterCriteria,
currentPage,
pageSize,
)
const adminUsers = await Promise.all(
users.map(async (user) => { users.map(async (user) => {
const adminUser = new UserAdmin() const adminUser = new UserAdmin()
adminUser.userId = user.id adminUser.userId = user.id
@ -57,6 +75,7 @@ export class AdminResolver {
updatedAt: 'DESC', updatedAt: 'DESC',
createdAt: 'DESC', createdAt: 'DESC',
}, },
select: ['updatedAt', 'createdAt'],
}, },
) )
if (emailOptIn) { if (emailOptIn) {
@ -70,11 +89,9 @@ export class AdminResolver {
return adminUser return adminUser
}), }),
) )
if (notActivated) adminUsers = adminUsers.filter((u) => !u.emailChecked)
const first = (currentPage - 1) * pageSize
return { return {
userCount: adminUsers.length, userCount: count,
userList: adminUsers.slice(first, first + pageSize), userList: adminUsers,
} }
} }
@ -83,8 +100,13 @@ export class AdminResolver {
async createPendingCreation( async createPendingCreation(
@Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs, @Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
): Promise<number[]> { ): Promise<number[]> {
const userRepository = getCustomRepository(UserRepository) const user = await User.findOne({ email }, { withDeleted: true })
const user = await userRepository.findByEmail(email) if (!user) {
throw new Error(`Could not find user with email: ${email}`)
}
if (user.deletedAt) {
throw new Error('This user was deleted. Cannot make a creation.')
}
if (!user.emailChecked) { if (!user.emailChecked) {
throw new Error('Creation could not be saved, Email is not activated') throw new Error('Creation could not be saved, Email is not activated')
} }
@ -135,8 +157,13 @@ export class AdminResolver {
async updatePendingCreation( async updatePendingCreation(
@Args() { id, email, amount, memo, creationDate, moderator }: UpdatePendingCreationArgs, @Args() { id, email, amount, memo, creationDate, moderator }: UpdatePendingCreationArgs,
): Promise<UpdatePendingCreation> { ): Promise<UpdatePendingCreation> {
const userRepository = getCustomRepository(UserRepository) const user = await User.findOne({ email }, { withDeleted: true })
const user = await userRepository.findByEmail(email) if (!user) {
throw new Error(`Could not find user with email: ${email}`)
}
if (user.deletedAt) {
throw new Error(`User was deleted (${email})`)
}
const pendingCreationToUpdate = await AdminPendingCreation.findOneOrFail({ id }) const pendingCreationToUpdate = await AdminPendingCreation.findOneOrFail({ id })
@ -213,23 +240,17 @@ export class AdminResolver {
if (moderatorUser.id === pendingCreation.userId) if (moderatorUser.id === pendingCreation.userId)
throw new Error('Moderator can not confirm own pending creation') throw new Error('Moderator can not confirm own pending creation')
const transactionRepository = getCustomRepository(TransactionRepository)
const receivedCallDate = new Date() const receivedCallDate = new Date()
let transaction = new Transaction() let transaction = new Transaction()
transaction.transactionTypeId = 1 transaction.transactionTypeId = TransactionTypeId.CREATION
transaction.memo = pendingCreation.memo transaction.memo = pendingCreation.memo
transaction.received = receivedCallDate transaction.received = receivedCallDate
transaction = await transactionRepository.save(transaction) 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') if (!transaction) throw new Error('Could not create transaction')
let transactionCreation = new TransactionCreation()
transactionCreation.transactionId = transaction.id
transactionCreation.userId = pendingCreation.userId
transactionCreation.amount = parseInt(pendingCreation.amount.toString())
transactionCreation.targetDate = pendingCreation.date
transactionCreation = await TransactionCreation.save(transactionCreation)
if (!transactionCreation) throw new Error('Could not create transactionCreation')
const userTransactionRepository = getCustomRepository(UserTransactionRepository) const userTransactionRepository = getCustomRepository(UserTransactionRepository)
const lastUserTransaction = await userTransactionRepository.findLastForUser( const lastUserTransaction = await userTransactionRepository.findLastForUser(
pendingCreation.userId, pendingCreation.userId,
@ -257,15 +278,15 @@ export class AdminResolver {
throw new Error('Error saving user transaction: ' + error) throw new Error('Error saving user transaction: ' + error)
}) })
const balanceRepository = getCustomRepository(BalanceRepository) let userBalance = await Balance.findOne({ userId: pendingCreation.userId })
let userBalance = await balanceRepository.findByUser(pendingCreation.userId) if (!userBalance) {
userBalance = new Balance()
if (!userBalance) userBalance = balanceRepository.create() userBalance.userId = pendingCreation.userId
userBalance.userId = pendingCreation.userId }
userBalance.amount = Number(newBalance) userBalance.amount = Number(newBalance)
userBalance.modified = receivedCallDate userBalance.modified = receivedCallDate
userBalance.recordDate = receivedCallDate userBalance.recordDate = receivedCallDate
await balanceRepository.save(userBalance) await userBalance.save()
await AdminPendingCreation.delete(pendingCreation) await AdminPendingCreation.delete(pendingCreation)
return true return true
@ -279,12 +300,13 @@ async function getUserCreations(id: number): Promise<number[]> {
const lastMonthNumber = moment().subtract(1, 'month').format('M') const lastMonthNumber = moment().subtract(1, 'month').format('M')
const currentMonthNumber = moment().format('M') const currentMonthNumber = moment().format('M')
const createdAmountsQuery = await TransactionCreation.createQueryBuilder('transaction_creations') const createdAmountsQuery = await Transaction.createQueryBuilder('transactions')
.select('MONTH(transaction_creations.target_date)', 'target_month') .select('MONTH(transactions.creation_date)', 'target_month')
.addSelect('SUM(transaction_creations.amount)', 'sum') .addSelect('SUM(transactions.amount)', 'sum')
.where('transaction_creations.state_user_id = :id', { id }) .where('transactions.user_id = :id', { id })
.andWhere('transactions.transaction_type_id = :type', { type: TransactionTypeId.CREATION })
.andWhere({ .andWhere({
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, { creationDate: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
date: dateBeforeLastMonth, date: dateBeforeLastMonth,
endDate: dateNextMonth, endDate: dateNextMonth,
}), }),

View File

@ -4,11 +4,11 @@
import { Resolver, Query, Ctx, Authorized } from 'type-graphql' import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { getCustomRepository } from '@dbTools/typeorm' import { getCustomRepository } from '@dbTools/typeorm'
import { Balance } from '../model/Balance' import { Balance } from '../model/Balance'
import { BalanceRepository } from '../../typeorm/repository/Balance'
import { UserRepository } from '../../typeorm/repository/User' import { UserRepository } from '../../typeorm/repository/User'
import { calculateDecay } from '../../util/decay' import { calculateDecay } from '../../util/decay'
import { roundFloorFrom4 } from '../../util/round' import { roundFloorFrom4 } from '../../util/round'
import { RIGHTS } from '../../auth/RIGHTS' import { RIGHTS } from '../../auth/RIGHTS'
import { Balance as dbBalance } from '@entity/Balance'
@Resolver() @Resolver()
export class BalanceResolver { export class BalanceResolver {
@ -16,11 +16,10 @@ export class BalanceResolver {
@Query(() => Balance) @Query(() => Balance)
async balance(@Ctx() context: any): Promise<Balance> { async balance(@Ctx() context: any): Promise<Balance> {
// load user and balance // load user and balance
const balanceRepository = getCustomRepository(BalanceRepository)
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey) const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const balanceEntity = await balanceRepository.findByUser(userEntity.id) const balanceEntity = await dbBalance.findOne({ userId: userEntity.id })
const now = new Date() const now = new Date()
// No balance found // No balance found

View File

@ -3,7 +3,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
import { getCustomRepository, getConnection, QueryRunner } from '@dbTools/typeorm' import { getCustomRepository, getConnection, QueryRunner, In } from '@dbTools/typeorm'
import CONFIG from '../../config' import CONFIG from '../../config'
import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail' import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
@ -16,15 +16,12 @@ import Paginated from '../arg/Paginated'
import { Order } from '../enum/Order' import { Order } from '../enum/Order'
import { BalanceRepository } from '../../typeorm/repository/Balance'
import { UserRepository } from '../../typeorm/repository/User' import { UserRepository } from '../../typeorm/repository/User'
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction' import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
import { TransactionRepository } from '../../typeorm/repository/Transaction'
import { User as dbUser } from '@entity/User' import { User as dbUser } from '@entity/User'
import { UserTransaction as dbUserTransaction } from '@entity/UserTransaction' import { UserTransaction as dbUserTransaction } from '@entity/UserTransaction'
import { Transaction as dbTransaction } from '@entity/Transaction' import { Transaction as dbTransaction } from '@entity/Transaction'
import { TransactionSendCoin as dbTransactionSendCoin } from '@entity/TransactionSendCoin'
import { Balance as dbBalance } from '@entity/Balance' import { Balance as dbBalance } from '@entity/Balance'
import { apiPost } from '../../apis/HttpRequest' import { apiPost } from '../../apis/HttpRequest'
@ -50,15 +47,13 @@ async function calculateAndAddDecayTransactions(
transactionIds.push(userTransaction.transactionId) transactionIds.push(userTransaction.transactionId)
}) })
const transactionRepository = getCustomRepository(TransactionRepository) const transactions = await dbTransaction.find({ where: { id: In(transactionIds) } })
const transactions = await transactionRepository.joinFullTransactionsByIds(transactionIds)
const transactionIndiced: dbTransaction[] = [] const transactionIndiced: dbTransaction[] = []
transactions.forEach((transaction: dbTransaction) => { transactions.forEach((transaction: dbTransaction) => {
transactionIndiced[transaction.id] = transaction transactionIndiced[transaction.id] = transaction
involvedUserIds.push(transaction.userId)
if (transaction.transactionTypeId === TransactionTypeId.SEND) { if (transaction.transactionTypeId === TransactionTypeId.SEND) {
involvedUserIds.push(transaction.transactionSendCoin.userId) involvedUserIds.push(transaction.sendReceiverUserId!) // TODO ensure not null properly
involvedUserIds.push(transaction.transactionSendCoin.recipiantUserId)
} }
}) })
// remove duplicates // remove duplicates
@ -108,24 +103,21 @@ async function calculateAndAddDecayTransactions(
// balance // balance
if (userTransaction.transactionTypeId === TransactionTypeId.CREATION) { if (userTransaction.transactionTypeId === TransactionTypeId.CREATION) {
// creation // creation
const creation = transaction.transactionCreation
finalTransaction.name = 'Gradido Akademie' finalTransaction.name = 'Gradido Akademie'
finalTransaction.type = TransactionType.CREATION finalTransaction.type = TransactionType.CREATION
// finalTransaction.targetDate = creation.targetDate // finalTransaction.targetDate = creation.targetDate
finalTransaction.balance = roundFloorFrom4(creation.amount) finalTransaction.balance = roundFloorFrom4(Number(transaction.amount)) // Todo unsafe conversion
} else if (userTransaction.transactionTypeId === TransactionTypeId.SEND) { } else if (userTransaction.transactionTypeId === TransactionTypeId.SEND) {
// send coin // send coin
const sendCoin = transaction.transactionSendCoin
let otherUser: dbUser | undefined let otherUser: dbUser | undefined
finalTransaction.balance = roundFloorFrom4(sendCoin.amount) finalTransaction.balance = roundFloorFrom4(Number(transaction.amount)) // Todo unsafe conversion
if (sendCoin.userId === user.id) { if (transaction.userId === user.id) {
finalTransaction.type = TransactionType.SEND finalTransaction.type = TransactionType.SEND
otherUser = userIndiced[sendCoin.recipiantUserId] otherUser = userIndiced.find((u) => u.id === transaction.sendReceiverUserId)
// finalTransaction.pubkey = sendCoin.recipiantPublic // finalTransaction.pubkey = sendCoin.recipiantPublic
} else if (sendCoin.recipiantUserId === user.id) { } else if (transaction.sendReceiverUserId === user.id) {
finalTransaction.type = TransactionType.RECIEVE finalTransaction.type = TransactionType.RECIEVE
otherUser = userIndiced[sendCoin.userId] otherUser = userIndiced.find((u) => u.id === transaction.userId)
// finalTransaction.pubkey = sendCoin.senderPublic // finalTransaction.pubkey = sendCoin.senderPublic
} else { } else {
throw new Error('invalid transaction') throw new Error('invalid transaction')
@ -153,61 +145,9 @@ async function calculateAndAddDecayTransactions(
finalTransactions.push(decayTransaction) finalTransactions.push(decayTransaction)
} }
} }
return finalTransactions return finalTransactions
} }
// Helper function
async function listTransactions(
currentPage: number,
pageSize: number,
order: Order,
user: dbUser,
onlyCreations: boolean,
): Promise<TransactionList> {
let limit = pageSize
let offset = 0
let skipFirstTransaction = false
if (currentPage > 1) {
offset = (currentPage - 1) * pageSize - 1
limit++
}
if (offset && order === Order.ASC) {
offset--
}
const userTransactionRepository = getCustomRepository(UserTransactionRepository)
let [userTransactions, userTransactionsCount] = await userTransactionRepository.findByUserPaged(
user.id,
limit,
offset,
order,
onlyCreations,
)
skipFirstTransaction = userTransactionsCount > offset + limit
const decay = !(currentPage > 1)
let transactions: Transaction[] = []
if (userTransactions.length) {
if (order === Order.DESC) {
userTransactions = userTransactions.reverse()
}
transactions = await calculateAndAddDecayTransactions(
userTransactions,
user,
decay,
skipFirstTransaction,
)
if (order === Order.DESC) {
transactions = transactions.reverse()
}
}
const transactionList = new TransactionList()
transactionList.count = userTransactionsCount
transactionList.transactions = transactions
return transactionList
}
// helper helper function // helper helper function
async function updateStateBalance( async function updateStateBalance(
user: dbUser, user: dbUser,
@ -215,8 +155,7 @@ async function updateStateBalance(
received: Date, received: Date,
queryRunner: QueryRunner, queryRunner: QueryRunner,
): Promise<dbBalance> { ): Promise<dbBalance> {
const balanceRepository = getCustomRepository(BalanceRepository) let balance = await dbBalance.findOne({ userId: user.id })
let balance = await balanceRepository.findByUser(user.id)
if (!balance) { if (!balance) {
balance = new dbBalance() balance = new dbBalance()
balance.userId = user.id balance.userId = user.id
@ -272,16 +211,6 @@ async function addUserTransaction(
}) })
} }
async function getPublicKey(email: string): Promise<string | null> {
const user = await dbUser.findOne({ email: email })
// User not found
if (!user) {
return null
}
return user.pubKey.toString('hex')
}
@Resolver() @Resolver()
export class TransactionResolver { export class TransactionResolver {
@Authorized([RIGHTS.TRANSACTION_LIST]) @Authorized([RIGHTS.TRANSACTION_LIST])
@ -299,43 +228,66 @@ export class TransactionResolver {
): Promise<TransactionList> { ): Promise<TransactionList> {
// load user // load user
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
let userEntity: dbUser | undefined const user = userId
if (userId) { ? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
userEntity = await userRepository.findOneOrFail({ id: userId }) : await userRepository.findByPubkeyHex(context.pubKey)
} else { let limit = pageSize
userEntity = await userRepository.findByPubkeyHex(context.pubKey) let offset = 0
let skipFirstTransaction = false
if (currentPage > 1) {
offset = (currentPage - 1) * pageSize - 1
limit++
}
if (offset && order === Order.ASC) {
offset--
}
const userTransactionRepository = getCustomRepository(UserTransactionRepository)
const [userTransactions, userTransactionsCount] =
await userTransactionRepository.findByUserPaged(user.id, limit, offset, order, onlyCreations)
skipFirstTransaction = userTransactionsCount > offset + limit
const decay = !(currentPage > 1)
let transactions: Transaction[] = []
if (userTransactions.length) {
if (order === Order.DESC) {
userTransactions.reverse()
}
transactions = await calculateAndAddDecayTransactions(
userTransactions,
user,
decay,
skipFirstTransaction,
)
if (order === Order.DESC) {
transactions.reverse()
}
} }
const transactions = await listTransactions( const transactionList = new TransactionList()
currentPage, transactionList.count = userTransactionsCount
pageSize, transactionList.transactions = transactions
order,
userEntity,
onlyCreations,
)
// get gdt sum // get gdt sum
transactions.gdtSum = null transactionList.gdtSum = null
try { try {
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, { const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
email: userEntity.email, email: user.email,
}) })
if (resultGDTSum.success) transactions.gdtSum = Number(resultGDTSum.data.sum) || 0 if (resultGDTSum.success) transactionList.gdtSum = Number(resultGDTSum.data.sum) || 0
} catch (err: any) {} } catch (err: any) {}
// get balance // get balance
const balanceRepository = getCustomRepository(BalanceRepository) const balanceEntity = await dbBalance.findOne({ userId: user.id })
const balanceEntity = await balanceRepository.findByUser(userEntity.id)
if (balanceEntity) { if (balanceEntity) {
const now = new Date() const now = new Date()
transactions.balance = roundFloorFrom4(balanceEntity.amount) transactionList.balance = roundFloorFrom4(balanceEntity.amount)
transactions.decay = roundFloorFrom4( // TODO: Add a decay object here instead of static data representing the decay.
transactionList.decay = roundFloorFrom4(
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance, calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
) )
transactions.decayDate = now.toString() transactionList.decayDate = now.toString()
} }
return transactions return transactionList
} }
@Authorized([RIGHTS.SEND_COINS]) @Authorized([RIGHTS.SEND_COINS])
@ -343,37 +295,28 @@ export class TransactionResolver {
async sendCoins( async sendCoins(
@Args() { email, amount, memo }: TransactionSendArgs, @Args() { email, amount, memo }: TransactionSendArgs,
@Ctx() context: any, @Ctx() context: any,
): Promise<string> { ): Promise<boolean> {
// TODO this is subject to replay attacks // TODO this is subject to replay attacks
// validate sender user (logged in)
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const senderUser = await userRepository.findByPubkeyHex(context.pubKey) const senderUser = await userRepository.findByPubkeyHex(context.pubKey)
if (senderUser.pubKey.length !== 32) { if (senderUser.pubKey.length !== 32) {
throw new Error('invalid sender public key') throw new Error('invalid sender public key')
} }
// validate amount
if (!hasUserAmount(senderUser, amount)) { if (!hasUserAmount(senderUser, amount)) {
throw new Error("user hasn't enough GDD") throw new Error("user hasn't enough GDD or amount is < 0")
} }
// validate recipient user // validate recipient user
// TODO: the detour over the public key is unnecessary const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
const recipiantPublicKey = await getPublicKey(email) if (!recipientUser) {
if (!recipiantPublicKey) {
throw new Error('recipient not known') throw new Error('recipient not known')
} }
if (!isHexPublicKey(recipiantPublicKey)) { if (recipientUser.deletedAt) {
throw new Error('invalid recipiant public key') throw new Error('The recipient account was deleted')
} }
const recipiantUser = await userRepository.findByPubkeyHex(recipiantPublicKey) if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) {
if (!recipiantUser) { throw new Error('invalid recipient public key')
throw new Error('Cannot find recipiant user by local send coins transaction')
} else if (recipiantUser.deletedAt) {
throw new Error('recipiant user account is disabled')
}
// validate amount
if (amount <= 0) {
throw new Error('invalid amount')
} }
const centAmount = Math.trunc(amount * 10000) const centAmount = Math.trunc(amount * 10000)
@ -383,17 +326,16 @@ export class TransactionResolver {
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
try { try {
// transaction // transaction
let transaction = new dbTransaction() const transaction = new dbTransaction()
transaction.transactionTypeId = TransactionTypeId.SEND transaction.transactionTypeId = TransactionTypeId.SEND
transaction.memo = memo transaction.memo = memo
transaction.userId = senderUser.id
transaction.pubkey = senderUser.pubKey
transaction.sendReceiverUserId = recipientUser.id
transaction.sendReceiverPublicKey = recipientUser.pubKey
transaction.amount = BigInt(centAmount)
// TODO: NO! this is problematic in its construction await queryRunner.manager.insert(dbTransaction, transaction)
const insertResult = await queryRunner.manager.insert(dbTransaction, transaction)
transaction = await queryRunner.manager
.findOneOrFail(dbTransaction, insertResult.generatedMaps[0].id)
.catch((error) => {
throw new Error('error loading saved transaction: ' + error)
})
// Insert Transaction: sender - amount // Insert Transaction: sender - amount
const senderUserTransactionBalance = await addUserTransaction( const senderUserTransactionBalance = await addUserTransaction(
@ -405,7 +347,7 @@ export class TransactionResolver {
// Insert Transaction: recipient + amount // Insert Transaction: recipient + amount
const recipiantUserTransactionBalance = await addUserTransaction( const recipiantUserTransactionBalance = await addUserTransaction(
recipiantUser, recipientUser,
transaction, transaction,
centAmount, centAmount,
queryRunner, queryRunner,
@ -421,7 +363,7 @@ export class TransactionResolver {
// Update Balance: recipiant + amount // Update Balance: recipiant + amount
const recipiantStateBalance = await updateStateBalance( const recipiantStateBalance = await updateStateBalance(
recipiantUser, recipientUser,
centAmount, centAmount,
transaction.received, transaction.received,
queryRunner, queryRunner,
@ -434,18 +376,10 @@ export class TransactionResolver {
throw new Error('db data corrupted, recipiant') throw new Error('db data corrupted, recipiant')
} }
// transactionSendCoin // TODO: WTF?
const transactionSendCoin = new dbTransactionSendCoin() // I just assume that due to implicit type conversion the decimal places were cut.
transactionSendCoin.transactionId = transaction.id // Using `Math.trunc` to simulate this behaviour
transactionSendCoin.userId = senderUser.id transaction.sendSenderFinalBalance = BigInt(Math.trunc(senderStateBalance.amount))
transactionSendCoin.senderPublic = senderUser.pubKey
transactionSendCoin.recipiantUserId = recipiantUser.id
transactionSendCoin.recipiantPublic = Buffer.from(recipiantPublicKey, 'hex')
transactionSendCoin.amount = centAmount
transactionSendCoin.senderFinalBalance = senderStateBalance.amount
await queryRunner.manager.save(transactionSendCoin).catch((error) => {
throw new Error('error saving transaction send coin: ' + error)
})
await queryRunner.manager.save(transaction).catch((error) => { await queryRunner.manager.save(transaction).catch((error) => {
throw new Error('error saving transaction with tx hash: ' + error) throw new Error('error saving transaction with tx hash: ' + error)
@ -474,13 +408,13 @@ export class TransactionResolver {
await sendTransactionReceivedEmail({ await sendTransactionReceivedEmail({
senderFirstName: senderUser.firstName, senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName, senderLastName: senderUser.lastName,
recipientFirstName: recipiantUser.firstName, recipientFirstName: recipientUser.firstName,
recipientLastName: recipiantUser.lastName, recipientLastName: recipientUser.lastName,
email: recipiantUser.email, email: recipientUser.email,
amount, amount,
memo, memo,
}) })
return 'success' return true
} }
} }

View File

@ -6,14 +6,13 @@ import gql from 'graphql-tag'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import createServer from '../../server/createServer' import createServer from '../../server/createServer'
import { resetDB, initialize } from '@dbTools/helpers' import { resetDB, initialize } from '@dbTools/helpers'
import { getRepository } from 'typeorm'
import { LoginUser } from '@entity/LoginUser'
import { LoginUserBackup } from '@entity/LoginUserBackup'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { User } from '@entity/User' import { User } from '@entity/User'
import CONFIG from '../../config' import CONFIG from '../../config'
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail' import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
import { klicktippSignIn } from '../../apis/KlicktippController' // import { klicktippSignIn } from '../../apis/KlicktippController'
jest.setTimeout(10000)
jest.mock('../../mailer/sendAccountActivationEmail', () => { jest.mock('../../mailer/sendAccountActivationEmail', () => {
return { return {
@ -22,12 +21,14 @@ jest.mock('../../mailer/sendAccountActivationEmail', () => {
} }
}) })
/*
jest.mock('../../apis/KlicktippController', () => { jest.mock('../../apis/KlicktippController', () => {
return { return {
__esModule: true, __esModule: true,
klicktippSignIn: jest.fn(), klicktippSignIn: jest.fn(),
} }
}) })
*/
let mutate: any let mutate: any
let con: any let con: any
@ -40,6 +41,11 @@ beforeAll(async () => {
await resetDB() await resetDB()
}) })
afterAll(async () => {
await resetDB(true)
await con.close()
})
describe('UserResolver', () => { describe('UserResolver', () => {
describe('createUser', () => { describe('createUser', () => {
const variables = { const variables = {
@ -84,70 +90,32 @@ describe('UserResolver', () => {
}) })
describe('valid input data', () => { describe('valid input data', () => {
let loginUser: LoginUser[]
let user: User[] let user: User[]
let loginUserBackup: LoginUserBackup[]
let loginEmailOptIn: LoginEmailOptIn[] let loginEmailOptIn: LoginEmailOptIn[]
beforeAll(async () => { beforeAll(async () => {
loginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany() user = await User.find()
user = await getRepository(User).createQueryBuilder('state_user').getMany() loginEmailOptIn = await LoginEmailOptIn.find()
loginUserBackup = await getRepository(LoginUserBackup)
.createQueryBuilder('login_user_backup')
.getMany()
loginEmailOptIn = await getRepository(LoginEmailOptIn)
.createQueryBuilder('login_email_optin')
.getMany()
emailOptIn = loginEmailOptIn[0].verificationCode.toString() emailOptIn = loginEmailOptIn[0].verificationCode.toString()
}) })
describe('filling all tables', () => { describe('filling all tables', () => {
it('saves the user in login_user table', () => { it('saves the user in login_user table', () => {
expect(loginUser).toEqual([ expect(user).toEqual([
{ {
id: expect.any(Number), id: expect.any(Number),
email: 'peter@lustig.de', email: 'peter@lustig.de',
firstName: 'Peter', firstName: 'Peter',
lastName: 'Lustig', lastName: 'Lustig',
username: '',
description: '',
password: '0', password: '0',
pubKey: null, pubKey: null,
privKey: null, privKey: null,
emailHash: expect.any(Buffer), emailHash: expect.any(Buffer),
createdAt: expect.any(Date), createdAt: expect.any(Date),
emailChecked: false, emailChecked: false,
passphraseShown: false,
language: 'de',
disabled: false,
groupId: 1,
publisherId: 1234,
},
])
})
it('saves the user in state_user table', () => {
expect(user).toEqual([
{
id: expect.any(Number),
indexId: 0,
groupId: 0,
pubkey: expect.any(Buffer),
email: 'peter@lustig.de',
firstName: 'Peter',
lastName: 'Lustig',
username: '',
disabled: false,
},
])
})
it('saves the user in login_user_backup table', () => {
expect(loginUserBackup).toEqual([
{
id: expect.any(Number),
passphrase: expect.any(String), passphrase: expect.any(String),
userId: loginUser[0].id, language: 'de',
mnemonicType: 2, deletedAt: null,
publisherId: 1234,
}, },
]) ])
}) })
@ -156,7 +124,7 @@ describe('UserResolver', () => {
expect(loginEmailOptIn).toEqual([ expect(loginEmailOptIn).toEqual([
{ {
id: expect.any(Number), id: expect.any(Number),
userId: loginUser[0].id, userId: user[0].id,
verificationCode: expect.any(String), verificationCode: expect.any(String),
emailOptInTypeId: 1, emailOptInTypeId: 1,
createdAt: expect.any(Date), createdAt: expect.any(Date),
@ -196,9 +164,7 @@ describe('UserResolver', () => {
mutation, mutation,
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' }, variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' },
}) })
await expect( await expect(User.find()).resolves.toEqual(
getRepository(LoginUser).createQueryBuilder('login_user').getMany(),
).resolves.toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
@ -215,9 +181,7 @@ describe('UserResolver', () => {
mutation, mutation,
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined }, variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
}) })
await expect( await expect(User.find()).resolves.toEqual(
getRepository(LoginUser).createQueryBuilder('login_user').getMany(),
).resolves.toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
email: 'raeuber@hotzenplotz.de', email: 'raeuber@hotzenplotz.de',
@ -265,23 +229,17 @@ describe('UserResolver', () => {
let emailOptIn: string let emailOptIn: string
describe('valid optin code and valid password', () => { describe('valid optin code and valid password', () => {
let loginUser: any
let newLoginUser: any
let newUser: any let newUser: any
beforeAll(async () => { beforeAll(async () => {
await mutate({ mutation: createUserMutation, variables: createUserVariables }) await mutate({ mutation: createUserMutation, variables: createUserVariables })
const loginEmailOptIn = await getRepository(LoginEmailOptIn) const loginEmailOptIn = await LoginEmailOptIn.find()
.createQueryBuilder('login_email_optin')
.getMany()
loginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany()
emailOptIn = loginEmailOptIn[0].verificationCode.toString() emailOptIn = loginEmailOptIn[0].verificationCode.toString()
result = await mutate({ result = await mutate({
mutation: setPasswordMutation, mutation: setPasswordMutation,
variables: { code: emailOptIn, password: 'Aa12345_' }, variables: { code: emailOptIn, password: 'Aa12345_' },
}) })
newLoginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany() newUser = await User.find()
newUser = await getRepository(User).createQueryBuilder('state_user').getMany()
}) })
afterAll(async () => { afterAll(async () => {
@ -289,38 +247,27 @@ describe('UserResolver', () => {
}) })
it('sets email checked to true', () => { it('sets email checked to true', () => {
expect(newLoginUser[0].emailChecked).toBeTruthy() expect(newUser[0].emailChecked).toBeTruthy()
}) })
it('updates the password', () => { it('updates the password', () => {
expect(newLoginUser[0].password).toEqual('3917921995996627700') expect(newUser[0].password).toEqual('3917921995996627700')
})
it('updates the public Key on both user tables', () => {
expect(newLoginUser[0].pubKey).toEqual(expect.any(Buffer))
expect(newLoginUser[0].pubKey).not.toEqual(loginUser[0].pubKey)
expect(newLoginUser[0].pubKey).toEqual(newUser[0].pubkey)
})
it('updates the private Key', () => {
expect(newLoginUser[0].privKey).toEqual(expect.any(Buffer))
expect(newLoginUser[0].privKey).not.toEqual(loginUser[0].privKey)
}) })
it('removes the optin', async () => { it('removes the optin', async () => {
await expect( await expect(LoginEmailOptIn.find()).resolves.toHaveLength(0)
getRepository(LoginEmailOptIn).createQueryBuilder('login_email_optin').getMany(),
).resolves.toHaveLength(0)
}) })
/*
it('calls the klicktipp API', () => { it('calls the klicktipp API', () => {
expect(klicktippSignIn).toBeCalledWith( expect(klicktippSignIn).toBeCalledWith(
loginUser[0].email, user[0].email,
loginUser[0].language, user[0].language,
loginUser[0].firstName, user[0].firstName,
loginUser[0].lastName, user[0].lastName,
) )
}) })
*/
it('returns true', () => { it('returns true', () => {
expect(result).toBeTruthy() expect(result).toBeTruthy()
@ -330,9 +277,7 @@ describe('UserResolver', () => {
describe('no valid password', () => { describe('no valid password', () => {
beforeAll(async () => { beforeAll(async () => {
await mutate({ mutation: createUserMutation, variables: createUserVariables }) await mutate({ mutation: createUserMutation, variables: createUserVariables })
const loginEmailOptIn = await getRepository(LoginEmailOptIn) const loginEmailOptIn = await LoginEmailOptIn.find()
.createQueryBuilder('login_email_optin')
.getMany()
emailOptIn = loginEmailOptIn[0].verificationCode.toString() emailOptIn = loginEmailOptIn[0].verificationCode.toString()
result = await mutate({ result = await mutate({
mutation: setPasswordMutation, mutation: setPasswordMutation,
@ -380,8 +325,3 @@ describe('UserResolver', () => {
}) })
}) })
}) })
afterAll(async () => {
await resetDB(true)
await con.close()
})

View File

@ -3,7 +3,7 @@
import fs from 'fs' import fs from 'fs'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection, getCustomRepository, getRepository, QueryRunner } from '@dbTools/typeorm' import { getConnection, getCustomRepository, QueryRunner } from '@dbTools/typeorm'
import CONFIG from '../../config' import CONFIG from '../../config'
import { User } from '../model/User' import { User } from '../model/User'
import { User as DbUser } from '@entity/User' import { User as DbUser } from '@entity/User'
@ -152,8 +152,7 @@ const createEmailOptIn = async (
loginUserId: number, loginUserId: number,
queryRunner: QueryRunner, queryRunner: QueryRunner,
): Promise<LoginEmailOptIn> => { ): Promise<LoginEmailOptIn> => {
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn) let emailOptIn = await LoginEmailOptIn.findOne({
let emailOptIn = await loginEmailOptInRepository.findOne({
userId: loginUserId, userId: loginUserId,
emailOptInTypeId: EMAIL_OPT_IN_REGISTER, emailOptInTypeId: EMAIL_OPT_IN_REGISTER,
}) })
@ -182,8 +181,7 @@ const createEmailOptIn = async (
} }
const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => { const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn) let optInCode = await LoginEmailOptIn.findOne({
let optInCode = await loginEmailOptInRepository.findOne({
userId: loginUserId, userId: loginUserId,
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD, emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
}) })
@ -205,7 +203,7 @@ const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
optInCode.userId = loginUserId optInCode.userId = loginUserId
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
} }
await loginEmailOptInRepository.save(optInCode) await LoginEmailOptIn.save(optInCode)
return optInCode return optInCode
} }
@ -250,9 +248,12 @@ export class UserResolver {
@Ctx() context: any, @Ctx() context: any,
): Promise<User> { ): Promise<User> {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const dbUser = await DbUser.findOneOrFail({ email }).catch(() => { const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
throw new Error('No user with this credentials') throw new Error('No user with this credentials')
}) })
if (dbUser.deletedAt) {
throw new Error('This user was permanently disabled. Contact support for questions.')
}
if (!dbUser.emailChecked) { if (!dbUser.emailChecked) {
throw new Error('User email not validated') throw new Error('User email not validated')
} }
@ -335,9 +336,9 @@ export class UserResolver {
// Validate email unique // Validate email unique
// TODO: i can register an email in upper/lower case twice // TODO: i can register an email in upper/lower case twice
const userRepository = getCustomRepository(UserRepository) // TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
const usersFound = await userRepository.count({ email }) const userFound = await DbUser.findOne({ email }, { withDeleted: true })
if (usersFound !== 0) { if (userFound) {
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
throw new Error(`User already exists.`) throw new Error(`User already exists.`)
} }
@ -487,12 +488,9 @@ export class UserResolver {
} }
// Load code // Load code
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn) const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
const optInCode = await loginEmailOptInRepository throw new Error('Could not login with emailVerificationCode')
.findOneOrFail({ verificationCode: code }) })
.catch(() => {
throw new Error('Could not login with emailVerificationCode')
})
// Code is only valid for 10minutes // Code is only valid for 10minutes
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime() const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()

View File

@ -4,40 +4,42 @@
import { ApolloLogPlugin, LogMutateData } from 'apollo-log' import { ApolloLogPlugin, LogMutateData } from 'apollo-log'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
const plugins = [ const setHeadersPlugin = {
{ requestDidStart() {
requestDidStart() { return {
return { willSendResponse(requestContext: any) {
willSendResponse(requestContext: any) { const { setHeaders = [] } = requestContext.context
const { setHeaders = [] } = requestContext.context setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
setHeaders.forEach(({ key, value }: { [key: string]: string }) => { if (requestContext.response.http.headers.get(key)) {
if (requestContext.response.http.headers.get(key)) { requestContext.response.http.headers.set(key, value)
requestContext.response.http.headers.set(key, value) } else {
} else { requestContext.response.http.headers.append(key, value)
requestContext.response.http.headers.append(key, value) }
} })
}) return requestContext
return requestContext },
}, }
}
},
}, },
ApolloLogPlugin({ }
mutate: (data: LogMutateData) => {
// We need to deep clone the object in order to not modify the actual request
const dataCopy = cloneDeep(data)
// mask password if part of the query const apolloLogPlugin = ApolloLogPlugin({
if (dataCopy.context.request.variables && dataCopy.context.request.variables.password) { mutate: (data: LogMutateData) => {
dataCopy.context.request.variables.password = '***' // We need to deep clone the object in order to not modify the actual request
} const dataCopy = cloneDeep(data)
// mask token at all times // mask password if part of the query
dataCopy.context.context.token = '***' if (dataCopy.context.request.variables && dataCopy.context.request.variables.password) {
dataCopy.context.request.variables.password = '***'
}
return dataCopy // mask token at all times
}, dataCopy.context.context.token = '***'
}),
] return dataCopy
},
})
const plugins =
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, apolloLogPlugin]
export default plugins export default plugins

View File

@ -1,9 +0,0 @@
import { EntityRepository, Repository } from '@dbTools/typeorm'
import { Balance } from '@entity/Balance'
@EntityRepository(Balance)
export class BalanceRepository extends Repository<Balance> {
findByUser(userId: number): Promise<Balance | undefined> {
return this.createQueryBuilder('balance').where('balance.userId = :userId', { userId }).getOne()
}
}

View File

@ -1,21 +0,0 @@
import { EntityRepository, Repository } from '@dbTools/typeorm'
import { Transaction } from '@entity/Transaction'
@EntityRepository(Transaction)
export class TransactionRepository extends Repository<Transaction> {
async joinFullTransactionsByIds(transactionIds: number[]): Promise<Transaction[]> {
return this.createQueryBuilder('transaction')
.where('transaction.id IN (:...transactions)', { transactions: transactionIds })
.leftJoinAndSelect(
'transaction.transactionSendCoin',
'transactionSendCoin',
// 'transactionSendCoin.transaction_id = transaction.id',
)
.leftJoinAndSelect(
'transaction.transactionCreation',
'transactionCreation',
// 'transactionSendCoin.transaction_id = transaction.id',
)
.getMany()
}
}

View File

@ -1,4 +1,4 @@
import { EntityRepository, Repository } from '@dbTools/typeorm' import { Brackets, EntityRepository, ObjectLiteral, Repository } from '@dbTools/typeorm'
import { User } from '@entity/User' import { User } from '@entity/User'
@EntityRepository(User) @EntityRepository(User)
@ -9,38 +9,39 @@ export class UserRepository extends Repository<User> {
.getOneOrFail() .getOneOrFail()
} }
async findByPubkeyHexBuffer(pubkeyHexBuffer: Buffer): Promise<User> {
const pubKeyString = pubkeyHexBuffer.toString('hex')
return await this.findByPubkeyHex(pubKeyString)
}
async findByEmail(email: string): Promise<User> {
return this.createQueryBuilder('user').where('user.email = :email', { email }).getOneOrFail()
}
async getUsersIndiced(userIds: number[]): Promise<User[]> { async getUsersIndiced(userIds: number[]): Promise<User[]> {
if (!userIds.length) return [] return this.createQueryBuilder('user')
const users = await this.createQueryBuilder('user') .withDeleted() // We need to show the name for deleted users for old transactions
.select(['user.id', 'user.firstName', 'user.lastName', 'user.email']) .select(['user.id', 'user.firstName', 'user.lastName', 'user.email'])
.where('user.id IN (:...users)', { users: userIds }) .where('user.id IN (:...userIds)', { userIds })
.getMany() .getMany()
const usersIndiced: User[] = []
users.forEach((value) => {
usersIndiced[value.id] = value
})
return usersIndiced
} }
async findBySearchCriteria(searchCriteria: string): Promise<User[]> { async findBySearchCriteriaPagedFiltered(
select: string[],
searchCriteria: string,
filterCriteria: ObjectLiteral[],
currentPage: number,
pageSize: number,
): Promise<[User[], number]> {
return await this.createQueryBuilder('user') return await this.createQueryBuilder('user')
.select(select)
.withDeleted()
.where( .where(
'user.firstName like :name or user.lastName like :lastName or user.email like :email', new Brackets((qb) => {
{ qb.where(
name: `%${searchCriteria}%`, 'user.firstName like :name or user.lastName like :lastName or user.email like :email',
lastName: `%${searchCriteria}%`, {
email: `%${searchCriteria}%`, name: `%${searchCriteria}%`,
}, lastName: `%${searchCriteria}%`,
email: `%${searchCriteria}%`,
},
)
}),
) )
.getMany() .andWhere(filterCriteria)
.take(pageSize)
.skip((currentPage - 1) * pageSize)
.getManyAndCount()
} }
} }

View File

@ -0,0 +1,6 @@
/* eslint-disable no-console */
// disable console.info for apollo log
// eslint-disable-next-line @typescript-eslint/no-empty-function
console.info = () => {}

View File

@ -1,6 +1,6 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm' import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
import { TransactionCreation } from '../TransactionCreation' import { TransactionCreation } from './TransactionCreation'
import { TransactionSendCoin } from '../TransactionSendCoin' import { TransactionSendCoin } from './TransactionSendCoin'
@Entity('transactions') @Entity('transactions')
export class Transaction extends BaseEntity { export class Transaction extends BaseEntity {

View File

@ -1,6 +1,6 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm' import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
import { TransactionCreation } from '../TransactionCreation' import { TransactionCreation } from '../0001-init_db/TransactionCreation'
import { TransactionSendCoin } from '../TransactionSendCoin' import { TransactionSendCoin } from '../0001-init_db/TransactionSendCoin'
@Entity('transactions') @Entity('transactions')
export class Transaction extends BaseEntity { export class Transaction extends BaseEntity {

View File

@ -0,0 +1,70 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity('transactions')
export class Transaction extends BaseEntity {
// TODO the id is defined as bigint(20) - there might be problems with that: https://github.com/typeorm/typeorm/issues/2400
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'transaction_type_id', unsigned: true, nullable: false })
transactionTypeId: number
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({ type: 'bigint', nullable: false })
amount: BigInt
@Column({ name: 'tx_hash', type: 'binary', length: 48, default: null, nullable: true })
txHash: Buffer
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ type: 'timestamp', nullable: false, default: () => 'CURRENT_TIMESTAMP' })
received: Date
@Column({ type: 'binary', length: 64, nullable: true, default: null })
signature: 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
@Column({ name: 'creation_date', type: 'timestamp', nullable: true, default: null })
creationDate: Date
@Column({
name: 'send_receiver_public_key',
type: 'binary',
length: 32,
nullable: true,
default: null,
})
sendReceiverPublicKey: Buffer | null
@Column({
name: 'send_receiver_user_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
sendReceiverUserId?: number | null
@Column({
name: 'send_sender_final_balance',
type: 'bigint',
nullable: true,
default: null,
})
sendSenderFinalBalance: BigInt | null
}

View File

@ -1 +1 @@
export { Transaction } from './0016-transaction_signatures/Transaction' export { Transaction } from './0024-combine_transaction_tables/Transaction'

View File

@ -1 +0,0 @@
export { TransactionCreation } from './0001-init_db/TransactionCreation'

View File

@ -1 +0,0 @@
export { TransactionSendCoin } from './0001-init_db/TransactionSendCoin'

View File

@ -4,8 +4,6 @@ import { LoginEmailOptIn } from './LoginEmailOptIn'
import { Migration } from './Migration' import { Migration } from './Migration'
import { ServerUser } from './ServerUser' import { ServerUser } from './ServerUser'
import { Transaction } from './Transaction' import { Transaction } from './Transaction'
import { TransactionCreation } from './TransactionCreation'
import { TransactionSendCoin } from './TransactionSendCoin'
import { User } from './User' import { User } from './User'
import { UserSetting } from './UserSetting' import { UserSetting } from './UserSetting'
import { UserTransaction } from './UserTransaction' import { UserTransaction } from './UserTransaction'
@ -19,8 +17,6 @@ export const entities = [
Migration, Migration,
ServerUser, ServerUser,
Transaction, Transaction,
TransactionCreation,
TransactionSendCoin,
User, User,
UserSetting, UserSetting,
UserTransaction, UserTransaction,

View File

@ -5,19 +5,20 @@
* This also removes the trailing space * This also removes the trailing space
*/ */
import fs from 'fs' import fs from 'fs'
import path from 'path'
const TARGET_MNEMONIC_TYPE = 2 const TARGET_MNEMONIC_TYPE = 2
const PHRASE_WORD_COUNT = 24 const PHRASE_WORD_COUNT = 24
const WORDS_MNEMONIC_0 = fs const WORDS_MNEMONIC_0 = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer18112.txt') .readFileSync(path.resolve(__dirname, '../src/config/mnemonic.uncompressed_buffer18112.txt'))
.toString() .toString()
.split(',') .split(',')
const WORDS_MNEMONIC_1 = fs const WORDS_MNEMONIC_1 = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer18113.txt') .readFileSync(path.resolve(__dirname, '../src/config/mnemonic.uncompressed_buffer18113.txt'))
.toString() .toString()
.split(',') .split(',')
const WORDS_MNEMONIC_2 = fs const WORDS_MNEMONIC_2 = fs
.readFileSync('src/config/mnemonic.uncompressed_buffer13116.txt') .readFileSync(path.resolve(__dirname, '../src/config/mnemonic.uncompressed_buffer13116.txt'))
.toString() .toString()
.split(',') .split(',')
const WORDS_MNEMONIC = [WORDS_MNEMONIC_0, WORDS_MNEMONIC_1, WORDS_MNEMONIC_2] const WORDS_MNEMONIC = [WORDS_MNEMONIC_0, WORDS_MNEMONIC_1, WORDS_MNEMONIC_2]

View File

@ -0,0 +1,125 @@
/* MIGRATION TO COMBINE ALL TRANSACTION TABLES
*
* Combine all transaction tables into one table with all data
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Create new `user_id` column (former `state_user_id`), with a temporary default of null
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `user_id` int(10) unsigned DEFAULT NULL AFTER `transaction_type_id`;',
)
// Create new `amount` column, with a temporary default of null
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `amount` bigint(20) DEFAULT NULL AFTER `user_id`;',
)
// Create new `creation_ident_hash` column (former `ident_hash`)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `creation_ident_hash` binary(32) DEFAULT NULL AFTER `pubkey`;',
)
// Create new `creation_date` column (former `target_date`)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `creation_date` timestamp NULL DEFAULT NULL AFTER `creation_ident_hash`;',
)
// Create new `send_receiver_public_key` column (former `receiver_public_key`)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `send_receiver_public_key` binary(32) DEFAULT NULL AFTER `creation_date`;',
)
// Create new `send_receiver_user_id` column (former `receiver_user_id`)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `send_receiver_user_id` int(10) unsigned DEFAULT NULL AFTER `send_receiver_public_key`;',
)
// Create new `send_sender_final_balance` column (former `sender_final_balance`)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `send_sender_final_balance` bigint(20) DEFAULT NULL AFTER `send_receiver_user_id`;',
)
// Insert Data from `transaction_creations`
await queryFn(`
UPDATE transactions
INNER JOIN transaction_creations ON transaction_creations.transaction_id = transactions.id
SET transactions.user_id = transaction_creations.state_user_id,
transactions.amount = transaction_creations.amount,
transactions.creation_ident_hash = transaction_creations.ident_hash,
transactions.creation_date = transaction_creations.target_date;
`)
// Insert Data from `transaction_send_coins`
// Note: we drop `sender_public_key` in favor of `pubkey` from the original `transactions` table
// the data from `transaction_send_coins` seems incomplete for half the dataset (zeroed pubkey)
// with one key being different.
await queryFn(`
UPDATE transactions
INNER JOIN transaction_send_coins ON transaction_send_coins.transaction_id = transactions.id
SET transactions.user_id = transaction_send_coins.state_user_id,
transactions.amount = transaction_send_coins.amount,
transactions.send_receiver_public_key = transaction_send_coins.receiver_public_key,
transactions.send_receiver_user_id = transaction_send_coins.receiver_user_id,
transactions.send_sender_final_balance = transaction_send_coins.sender_final_balance;
`)
// Modify defaults after our inserts
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `user_id` int(10) unsigned NOT NULL;')
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `amount` bigint(20) NOT NULL;')
// Drop table `transaction_creations`
await queryFn('DROP TABLE `transaction_creations`;')
// Drop table `transaction_send_coins`
await queryFn('DROP TABLE `transaction_send_coins`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE \`transaction_send_coins\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`transaction_id\` int(10) unsigned NOT NULL,
\`sender_public_key\` binary(32) NOT NULL,
\`state_user_id\` int(10) unsigned DEFAULT 0,
\`receiver_public_key\` binary(32) NOT NULL,
\`receiver_user_id\` int(10) unsigned DEFAULT 0,
\`amount\` bigint(20) NOT NULL,
\`sender_final_balance\` bigint(20) NOT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB AUTO_INCREMENT=659 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`)
await queryFn(`
CREATE TABLE \`transaction_creations\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`transaction_id\` int(10) unsigned NOT NULL,
\`state_user_id\` int(10) unsigned NOT NULL,
\`amount\` bigint(20) NOT NULL,
\`ident_hash\` binary(32) DEFAULT NULL,
\`target_date\` timestamp NULL DEFAULT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB AUTO_INCREMENT=2769 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`)
await queryFn(`
INSERT INTO transaction_send_coins
( transaction_id, sender_public_key, state_user_id,
receiver_public_key, receiver_user_id,
amount, sender_final_balance )
( SELECT id AS transaction_id, IF(pubkey, pubkey, 0x00000000000000000000000000000000) AS sender_public_key, user_id AS state_user_id,
send_receiver_public_key AS receiver_public_key, send_receiver_user_id AS receiver_user_id,
amount, send_sender_final_balance AS sender_final_balance
FROM transactions
WHERE transaction_type_id = 2 );
`)
await queryFn(`
INSERT INTO transaction_creations
( transaction_id, state_user_id,
amount, ident_hash, target_date )
( SELECT id AS transaction_id, user_id AS state_user_id,
amount, creation_ident_hash AS ident_hash, creation_date AS target_date
FROM transactions
WHERE transaction_type_id = 1 );
`)
await queryFn('ALTER TABLE `transactions` DROP COLUMN `send_sender_final_balance`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `send_receiver_user_id`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `send_receiver_public_key`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `creation_date`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `creation_ident_hash`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `amount`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_id`;')
}

View File

@ -8,7 +8,7 @@
"license": "MIT", "license": "MIT",
"private": false, "private": false,
"scripts": { "scripts": {
"build": "tsc --build", "build": "mkdir -p build/src/config/ && cp src/config/*.txt build/src/config/ && tsc --build",
"clean": "tsc --build --clean", "clean": "tsc --build --clean",
"up": "node build/src/index.js up", "up": "node build/src/index.js up",
"down": "node build/src/index.js down", "down": "node build/src/index.js down",

View File

@ -1,18 +0,0 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { TransactionCreation } from '../../entity/TransactionCreation'
import { TransactionCreationContext } from '../interface/TransactionContext'
define(TransactionCreation, (faker: typeof Faker, context?: TransactionCreationContext) => {
if (!context || !context.userId || !context.transaction) {
throw new Error('TransactionCreation: No userId and/or transaction present!')
}
const transactionCreation = new TransactionCreation()
transactionCreation.userId = context.userId
transactionCreation.amount = context.amount ? context.amount : 100000
transactionCreation.targetDate = context.targetDate ? context.targetDate : new Date()
transactionCreation.transaction = context.transaction
return transactionCreation
})

View File

@ -5,17 +5,24 @@ import { TransactionContext } from '../interface/TransactionContext'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
define(Transaction, (faker: typeof Faker, context?: TransactionContext) => { define(Transaction, (faker: typeof Faker, context?: TransactionContext) => {
if (!context) context = {} if (!context) {
throw new Error('TransactionContext not well defined.')
}
const transaction = new Transaction() const transaction = new Transaction()
transaction.transactionTypeId = context.transactionTypeId ? context.transactionTypeId : 2 transaction.transactionTypeId = context.transactionTypeId // || 2
transaction.txHash = context.txHash ? context.txHash : randomBytes(48) transaction.userId = context.userId
transaction.memo = context.memo || context.memo === '' ? context.memo : faker.lorem.sentence() transaction.amount = context.amount
transaction.received = context.received ? context.received : new Date() transaction.txHash = context.txHash || randomBytes(48)
transaction.signature = context.signature ? context.signature : randomBytes(64) transaction.memo = context.memo
transaction.pubkey = context.signaturePubkey ? context.signaturePubkey : randomBytes(32) transaction.received = context.received || new Date()
if (context.transactionSendCoin) transaction.transactionSendCoin = context.transactionSendCoin transaction.signature = context.signature || randomBytes(64)
if (context.transactionCreation) transaction.transactionCreation = context.transactionCreation 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.sendSenderFinalBalance = context.sendSenderFinalBalance || null
return transaction return transaction
}) })

View File

@ -1,7 +1,7 @@
import Faker from 'faker' import Faker from 'faker'
import { define } from 'typeorm-seeding' import { define } from 'typeorm-seeding'
import { User } from '../../entity/User' import { User } from '../../entity/User'
import { randomBytes, randomInt } from 'crypto' import { randomBytes } from 'crypto'
import { UserContext } from '../interface/UserContext' import { UserContext } from '../interface/UserContext'
define(User, (faker: typeof Faker, context?: UserContext) => { define(User, (faker: typeof Faker, context?: UserContext) => {

View File

@ -1,18 +1,20 @@
import { Transaction } from '../../entity/Transaction' import { Transaction } from '../../entity/Transaction'
import { TransactionSendCoin } from '../../entity/TransactionSendCoin'
import { TransactionCreation } from '../../entity/TransactionCreation'
import { User } from '../../entity/User' import { User } from '../../entity/User'
export interface TransactionContext { export interface TransactionContext {
transactionTypeId?: number transactionTypeId: number
userId: number
amount: BigInt
txHash?: Buffer txHash?: Buffer
memo?: string memo: string
received?: Date received?: Date
blockchainTypeId?: number
signature?: Buffer signature?: Buffer
signaturePubkey?: Buffer pubkey?: Buffer
transactionSendCoin?: TransactionSendCoin creationIdentHash?: Buffer
transactionCreation?: TransactionCreation creationDate?: Date
sendReceiverPublicKey?: Buffer
sendReceiverUserId?: number
sendSenderFinalBalance?: BigInt
} }
export interface BalanceContext { export interface BalanceContext {
@ -32,13 +34,6 @@ export interface TransactionSendCoinContext {
transaction?: Transaction transaction?: Transaction
} }
export interface TransactionCreationContext {
userId?: number
amount?: number
targetDate?: Date
transaction?: Transaction
}
export interface UserTransactionContext { export interface UserTransactionContext {
userId?: number userId?: number
transactionId?: number transactionId?: number

View File

@ -11,7 +11,6 @@ export interface UserInterface {
emailChecked?: boolean emailChecked?: boolean
language?: string language?: string
deletedAt?: Date deletedAt?: Date
groupId?: number
publisherId?: number publisherId?: number
passphrase?: string passphrase?: string
// from server user // from server user
@ -27,9 +26,8 @@ export interface UserInterface {
// balance // balance
balanceModified?: Date balanceModified?: Date
recordDate?: Date recordDate?: Date
targetDate?: Date creationDate?: Date
amount?: number amount?: number
creationTxHash?: Buffer creationTxHash?: Buffer
signature?: Buffer signature?: Buffer
signaturePubkey?: Buffer
} }

View File

@ -2,7 +2,6 @@ import { UserContext, ServerUserContext } from '../../interface/UserContext'
import { import {
BalanceContext, BalanceContext,
TransactionContext, TransactionContext,
TransactionCreationContext,
UserTransactionContext, UserTransactionContext,
} from '../../interface/TransactionContext' } from '../../interface/TransactionContext'
import { UserInterface } from '../../interface/UserInterface' import { UserInterface } from '../../interface/UserInterface'
@ -11,7 +10,6 @@ import { ServerUser } from '../../../entity/ServerUser'
import { Balance } from '../../../entity/Balance' import { Balance } from '../../../entity/Balance'
import { Transaction } from '../../../entity/Transaction' import { Transaction } from '../../../entity/Transaction'
import { UserTransaction } from '../../../entity/UserTransaction' import { UserTransaction } from '../../../entity/UserTransaction'
import { TransactionCreation } from '../../../entity/TransactionCreation'
import { Factory } from 'typeorm-seeding' import { Factory } from 'typeorm-seeding'
export const userSeeder = async (factory: Factory, userData: UserInterface): Promise<void> => { export const userSeeder = async (factory: Factory, userData: UserInterface): Promise<void> => {
@ -25,10 +23,7 @@ export const userSeeder = async (factory: Factory, userData: UserInterface): Pro
// create some GDD for the user // create some GDD for the user
await factory(Balance)(createBalanceContext(userData, user)).create() await factory(Balance)(createBalanceContext(userData, user)).create()
const transaction = await factory(Transaction)( const transaction = await factory(Transaction)(
createTransactionContext(userData, 1, 'Herzlich Willkommen bei Gradido!'), createTransactionContext(userData, user, 1, 'Herzlich Willkommen bei Gradido!'),
).create()
await factory(TransactionCreation)(
createTransactionCreationContext(userData, user, transaction),
).create() ).create()
await factory(UserTransaction)( await factory(UserTransaction)(
createUserTransactionContext(userData, user, transaction), createUserTransactionContext(userData, user, transaction),
@ -76,27 +71,18 @@ const createBalanceContext = (context: UserInterface, user: User): BalanceContex
const createTransactionContext = ( const createTransactionContext = (
context: UserInterface, context: UserInterface,
user: User,
type: number, type: number,
memo: string, memo: string,
): TransactionContext => { ): TransactionContext => {
return { return {
transactionTypeId: type, transactionTypeId: type,
userId: user.id,
amount: BigInt(context.amount || 100000),
txHash: context.creationTxHash, txHash: context.creationTxHash,
memo, memo,
received: context.recordDate, received: context.recordDate,
} creationDate: context.creationDate,
}
const createTransactionCreationContext = (
context: UserInterface,
user: User,
transaction: Transaction,
): TransactionCreationContext => {
return {
userId: user.id,
amount: context.amount,
targetDate: context.targetDate,
transaction,
} }
} }
@ -112,6 +98,6 @@ const createUserTransactionContext = (
balance: context.amount, balance: context.amount,
balanceDate: context.recordDate, balanceDate: context.recordDate,
signature: context.signature, signature: context.signature,
pubkey: context.signaturePubkey, pubkey: context.pubKey,
} }
} }

View File

@ -1,8 +1,9 @@
export const bibiBloxberg = { import { UserInterface } from '../../interface/UserInterface'
export const bibiBloxberg: UserInterface = {
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
firstName: 'Bibi', firstName: 'Bibi',
lastName: 'Bloxberg', lastName: 'Bloxberg',
username: 'bibi',
// description: 'Hex Hex', // description: 'Hex Hex',
password: BigInt('12825419584724616625'), password: BigInt('12825419584724616625'),
pubKey: Buffer.from('42de7e4754625b730018c3b4ea745a4d043d9d867af352d0f08871793dfa6743', 'hex'), pubKey: Buffer.from('42de7e4754625b730018c3b4ea745a4d043d9d867af352d0f08871793dfa6743', 'hex'),
@ -14,16 +15,13 @@ export const bibiBloxberg = {
createdAt: new Date('2021-11-26T11:32:16'), createdAt: new Date('2021-11-26T11:32:16'),
emailChecked: true, emailChecked: true,
language: 'de', language: 'de',
disabled: false,
groupId: 1,
passphrase: passphrase:
'knife normal level all hurdle crucial color avoid warrior stadium road bachelor affair topple hawk pottery right afford immune two ceiling budget glance hour ', 'knife normal level all hurdle crucial color avoid warrior stadium road bachelor affair topple hawk pottery right afford immune two ceiling budget glance hour ',
mnemonicType: 2,
isAdmin: false, isAdmin: false,
addBalance: true, addBalance: true,
balanceModified: new Date('2021-11-30T10:37:11'), balanceModified: new Date('2021-11-30T10:37:11'),
recordDate: new Date('2021-11-30T10:37:11'), recordDate: new Date('2021-11-30T10:37:11'),
targetDate: new Date('2021-08-01 00:00:00'), creationDate: new Date('2021-08-01 00:00:00'),
amount: 10000000, amount: 10000000,
creationTxHash: Buffer.from( creationTxHash: Buffer.from(
'51103dc0fc2ca5d5d75a9557a1e899304e5406cfdb1328d8df6414d527b0118100000000000000000000000000000000', '51103dc0fc2ca5d5d75a9557a1e899304e5406cfdb1328d8df6414d527b0118100000000000000000000000000000000',
@ -33,8 +31,4 @@ export const bibiBloxberg = {
'2a2c71f3e41adc060bbc3086577e2d57d24eeeb0a7727339c3f85aad813808f601d7e1df56a26e0929d2e67fc054fca429ccfa283ed2782185c7f009fe008f0c', '2a2c71f3e41adc060bbc3086577e2d57d24eeeb0a7727339c3f85aad813808f601d7e1df56a26e0929d2e67fc054fca429ccfa283ed2782185c7f009fe008f0c',
'hex', 'hex',
), ),
signaturePubkey: Buffer.from(
'7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770',
'hex',
),
} }

View File

@ -1,8 +1,9 @@
export const bobBaumeister = { import { UserInterface } from '../../interface/UserInterface'
export const bobBaumeister: UserInterface = {
email: 'bob@baumeister.de', email: 'bob@baumeister.de',
firstName: 'Bob', firstName: 'Bob',
lastName: 'der Baumeister', lastName: 'der Baumeister',
username: 'bob',
// description: 'Können wir das schaffen? Ja, wir schaffen das!', // description: 'Können wir das schaffen? Ja, wir schaffen das!',
password: BigInt('3296644341468822636'), password: BigInt('3296644341468822636'),
pubKey: Buffer.from('a509d9a146374fc975e3677db801ae8a4a83bff9dea96da64053ff6de6b2dd7e', 'hex'), pubKey: Buffer.from('a509d9a146374fc975e3677db801ae8a4a83bff9dea96da64053ff6de6b2dd7e', 'hex'),
@ -14,16 +15,13 @@ export const bobBaumeister = {
createdAt: new Date('2021-11-26T11:36:31'), createdAt: new Date('2021-11-26T11:36:31'),
emailChecked: true, emailChecked: true,
language: 'de', language: 'de',
disabled: false,
groupId: 1,
passphrase: passphrase:
'detail master source effort unable waste tilt flush domain orchard art truck hint barrel response gate impose peanut secret merry three uncle wink resource ', 'detail master source effort unable waste tilt flush domain orchard art truck hint barrel response gate impose peanut secret merry three uncle wink resource ',
mnemonicType: 2,
isAdmin: false, isAdmin: false,
addBalance: true, addBalance: true,
balanceModified: new Date('2021-11-30T10:37:14'), balanceModified: new Date('2021-11-30T10:37:14'),
recordDate: new Date('2021-11-30T10:37:14'), recordDate: new Date('2021-11-30T10:37:14'),
targetDate: new Date('2021-08-01 00:00:00'), creationDate: new Date('2021-08-01 00:00:00'),
amount: 10000000, amount: 10000000,
creationTxHash: Buffer.from( creationTxHash: Buffer.from(
'be095dc87acb94987e71168fee8ecbf50ecb43a180b1006e75d573b35725c69c00000000000000000000000000000000', 'be095dc87acb94987e71168fee8ecbf50ecb43a180b1006e75d573b35725c69c00000000000000000000000000000000',
@ -33,8 +31,4 @@ export const bobBaumeister = {
'1fbd6b9a3d359923b2501557f3bc79fa7e428127c8090fb16bc490b4d87870ab142b3817ddd902d22f0b26472a483233784a0e460c0622661752a13978903905', '1fbd6b9a3d359923b2501557f3bc79fa7e428127c8090fb16bc490b4d87870ab142b3817ddd902d22f0b26472a483233784a0e460c0622661752a13978903905',
'hex', 'hex',
), ),
signaturePubkey: Buffer.from(
'7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770',
'hex',
),
} }

View File

@ -1,8 +1,9 @@
export const garrickOllivander = { import { UserInterface } from '../../interface/UserInterface'
export const garrickOllivander: UserInterface = {
email: 'garrick@ollivander.com', email: 'garrick@ollivander.com',
firstName: 'Garrick', firstName: 'Garrick',
lastName: 'Ollivander', lastName: 'Ollivander',
username: 'garrick',
// description: `Curious ... curious ... // description: `Curious ... curious ...
// Renowned wandmaker Mr Ollivander owns the wand shop Ollivanders: Makers of Fine Wands Since 382 BC in Diagon Alley. His shop is widely considered the best place to purchase a wand.`, // Renowned wandmaker Mr Ollivander owns the wand shop Ollivanders: Makers of Fine Wands Since 382 BC in Diagon Alley. His shop is widely considered the best place to purchase a wand.`,
password: BigInt('0'), password: BigInt('0'),
@ -10,11 +11,8 @@ export const garrickOllivander = {
createdAt: new Date('2022-01-10T10:23:17'), createdAt: new Date('2022-01-10T10:23:17'),
emailChecked: false, emailChecked: false,
language: 'en', language: 'en',
disabled: false,
groupId: 1,
passphrase: passphrase:
'human glide theory clump wish history other duty door fringe neck industry ostrich equal plate diesel tornado neck people antenna door category moon hen ', 'human glide theory clump wish history other duty door fringe neck industry ostrich equal plate diesel tornado neck people antenna door category moon hen ',
mnemonicType: 2,
isAdmin: false, isAdmin: false,
addBalance: false, addBalance: false,
} }

View File

@ -1,8 +1,9 @@
export const peterLustig = { import { UserInterface } from '../../interface/UserInterface'
export const peterLustig: UserInterface = {
email: 'peter@lustig.de', email: 'peter@lustig.de',
firstName: 'Peter', firstName: 'Peter',
lastName: 'Lustig', lastName: 'Lustig',
username: 'peter',
// description: 'Latzhose und Nickelbrille', // description: 'Latzhose und Nickelbrille',
password: BigInt('3917921995996627700'), password: BigInt('3917921995996627700'),
pubKey: Buffer.from('7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770', 'hex'), pubKey: Buffer.from('7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770', 'hex'),
@ -14,11 +15,8 @@ export const peterLustig = {
createdAt: new Date('2020-11-25T10:48:43'), createdAt: new Date('2020-11-25T10:48:43'),
emailChecked: true, emailChecked: true,
language: 'de', language: 'de',
disabled: false,
groupId: 1,
passphrase: passphrase:
'okay property choice naive calm present weird increase stuff royal vibrant frame attend wood one else tribe pull hedgehog woman kitchen hawk snack smart ', 'okay property choice naive calm present weird increase stuff royal vibrant frame attend wood one else tribe pull hedgehog woman kitchen hawk snack smart ',
mnemonicType: 2,
role: 'admin', role: 'admin',
serverUserPassword: '$2y$10$TzIWLeZoKs251gwrhSQmHeKhKI/EQ4EV5ClfAT8Ufnb4lcUXPa5X.', serverUserPassword: '$2y$10$TzIWLeZoKs251gwrhSQmHeKhKI/EQ4EV5ClfAT8Ufnb4lcUXPa5X.',
activated: 1, activated: 1,

View File

@ -1,8 +1,9 @@
export const raeuberHotzenplotz = { import { UserInterface } from '../../interface/UserInterface'
export const raeuberHotzenplotz: UserInterface = {
email: 'raeuber@hotzenplotz.de', email: 'raeuber@hotzenplotz.de',
firstName: 'Räuber', firstName: 'Räuber',
lastName: 'Hotzenplotz', lastName: 'Hotzenplotz',
username: 'räuber',
// description: 'Pfefferpistole', // description: 'Pfefferpistole',
password: BigInt('12123692783243004812'), password: BigInt('12123692783243004812'),
pubKey: Buffer.from('d7c70f94234dff071d982aa8f41583876c356599773b5911b39080da2b8c2d2b', 'hex'), pubKey: Buffer.from('d7c70f94234dff071d982aa8f41583876c356599773b5911b39080da2b8c2d2b', 'hex'),
@ -14,16 +15,13 @@ export const raeuberHotzenplotz = {
createdAt: new Date('2021-11-26T11:32:16'), createdAt: new Date('2021-11-26T11:32:16'),
emailChecked: true, emailChecked: true,
language: 'de', language: 'de',
disabled: false,
groupId: 1,
passphrase: passphrase:
'gospel trip tenant mouse spider skill auto curious man video chief response same little over expire drum display fancy clinic keen throw urge basket ', 'gospel trip tenant mouse spider skill auto curious man video chief response same little over expire drum display fancy clinic keen throw urge basket ',
mnemonicType: 2,
isAdmin: false, isAdmin: false,
addBalance: true, addBalance: true,
balanceModified: new Date('2021-11-30T10:37:13'), balanceModified: new Date('2021-11-30T10:37:13'),
recordDate: new Date('2021-11-30T10:37:13'), recordDate: new Date('2021-11-30T10:37:13'),
targetDate: new Date('2021-08-01 00:00:00'), creationDate: new Date('2021-08-01 00:00:00'),
amount: 10000000, amount: 10000000,
creationTxHash: Buffer.from( creationTxHash: Buffer.from(
'23ba44fd84deb59b9f32969ad0cb18bfa4588be1bdb99c396888506474c16c1900000000000000000000000000000000', '23ba44fd84deb59b9f32969ad0cb18bfa4588be1bdb99c396888506474c16c1900000000000000000000000000000000',
@ -33,8 +31,4 @@ export const raeuberHotzenplotz = {
'756d3da061687c575d1dbc5073908f646aa5f498b0927b217c83b48af471450e571dfe8421fb8e1f1ebd1104526b7e7c6fa78684e2da59c8f7f5a8dc3d9e5b0b', '756d3da061687c575d1dbc5073908f646aa5f498b0927b217c83b48af471450e571dfe8421fb8e1f1ebd1104526b7e7c6fa78684e2da59c8f7f5a8dc3d9e5b0b',
'hex', 'hex',
), ),
signaturePubkey: Buffer.from(
'7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770',
'hex',
),
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="decayinformation"> <div class="decayinformation">
<span v-if="decaytyp === 'short'"> <span v-if="decaytyp === 'short'">
{{ decay ? ' - ' + $n(decay.balance, 'decimal') + ' ' + decayStartBlockTextShort : '' }} {{ decay ? ' ' + $n(decay.balance, 'decimal') : '' }}
</span> </span>
<div v-if="decaytyp === 'new'"> <div v-if="decaytyp === 'new'">
@ -19,14 +19,11 @@
<b-col cols="6"> <b-col cols="6">
<div v-if="decay.decayStartBlock > 0"> <div v-if="decay.decayStartBlock > 0">
<div class="display-4">{{ $t('decay.Starting_block_decay') }}</div> <div class="display-4">{{ $t('decay.Starting_block_decay') }}</div>
<div> <div>{{ $t('decay.decay_introduced') }} :</div>
{{ $t('decay.decay_introduced') }} :
{{ $d($moment.unix(decay.decayStart), 'long') }}
</div>
</div> </div>
<div> <div>
<span v-if="decay.decayStart"> <span v-if="decay.decayStart">
{{ $d($moment.unix(decay.decayStart), 'long') }} {{ $d(new Date(decay.decayStart * 1000), 'long') }}
{{ $i18n.locale === 'de' ? 'Uhr' : '' }} {{ $i18n.locale === 'de' ? 'Uhr' : '' }}
</span> </span>
</div> </div>
@ -59,7 +56,7 @@
<div>{{ $t('decay.decay') }}</div> <div>{{ $t('decay.decay') }}</div>
</b-col> </b-col>
<b-col cols="6"> <b-col cols="6">
<div>- {{ $n(decay.balance, 'decimal') }}</div> <div> {{ $n(decay.balance, 'decimal') }}</div>
</b-col> </b-col>
</b-row> </b-row>
<hr class="mt-2 mb-2" /> <hr class="mt-2 mb-2" />
@ -75,7 +72,7 @@
<div v-if="type === 'receive'">{{ $t('decay.received') }}</div> <div v-if="type === 'receive'">{{ $t('decay.received') }}</div>
</b-col> </b-col>
<b-col cols="6"> <b-col cols="6">
<div v-if="type === 'send'">- {{ $n(balance, 'decimal') }}</div> <div v-if="type === 'send'"> {{ $n(balance, 'decimal') }}</div>
<div v-if="type === 'receive'">+ {{ $n(balance, 'decimal') }}</div> <div v-if="type === 'receive'">+ {{ $n(balance, 'decimal') }}</div>
</b-col> </b-col>
</b-row> </b-row>
@ -85,7 +82,7 @@
<div>{{ $t('decay.decay') }}</div> <div>{{ $t('decay.decay') }}</div>
</b-col> </b-col>
<b-col cols="6"> <b-col cols="6">
<div>- {{ $n(decay.balance, 'decimal') }}</div> <div> {{ $n(decay.balance, 'decimal') }}</div>
</b-col> </b-col>
</b-row> </b-row>
<!-- Total--> <!-- Total-->
@ -95,13 +92,13 @@
</b-col> </b-col>
<b-col cols="6"> <b-col cols="6">
<div v-if="type === 'send'"> <div v-if="type === 'send'">
<b>- {{ $n(balance + decay.balance, 'decimal') }}</b> <b> {{ $n(balance + decay.balance, 'decimal') }}</b>
</div> </div>
<div v-if="type === 'receive'"> <div v-if="type === 'receive'">
<b>{{ $n(balance - decay.balance, 'decimal') }}</b> <b>{{ $n(balance - decay.balance, 'decimal') }}</b>
</div> </div>
<div v-if="type === 'creation'"> <div v-if="type === 'creation'">
<b>- {{ $n(balance - decay.balance, 'decimal') }}</b> <b> {{ $n(balance - decay.balance, 'decimal') }}</b>
</div> </div>
</b-col> </b-col>
</b-row> </b-row>
@ -124,17 +121,8 @@ export default {
decaytyp: { type: String, default: '' }, decaytyp: { type: String, default: '' },
}, },
computed: { computed: {
decayStartBlockTextShort() {
return this.decay.decayStartBlock
? this.$t('decay.decayStart') + this.$d(this.$moment.unix(this.decay.decayStartBlock))
: ''
},
duration() { duration() {
return this.$moment.duration( return this.$moment.duration((this.decay.decayEnd - this.decay.decayStart) * 1000)._data
this.$moment
.unix(new Date(this.decay.decayEnd))
.diff(this.$moment.unix(new Date(this.decay.decayStart))),
)._data
}, },
}, },
} }

View File

@ -50,7 +50,7 @@
{{ $t('form.date') }} {{ $t('form.date') }}
</b-col> </b-col>
<b-col cols="6"> <b-col cols="6">
{{ $d($moment(date), 'long') }} {{ $i18n.locale === 'de' ? 'Uhr' : '' }} {{ $d(new Date(date), 'long') }} {{ $i18n.locale === 'de' ? 'Uhr' : '' }}
</b-col> </b-col>
</b-row> </b-row>

View File

@ -69,9 +69,9 @@ const dateTimeFormats = {
}, },
long: { long: {
year: 'numeric', year: 'numeric',
month: 'short', month: 'long',
day: 'numeric', day: 'numeric',
weekday: 'short', weekday: 'long',
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
}, },
@ -84,9 +84,9 @@ const dateTimeFormats = {
}, },
long: { long: {
day: 'numeric', day: 'numeric',
month: 'short', month: 'long',
year: 'numeric', year: 'numeric',
weekday: 'short', weekday: 'long',
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
}, },

View File

@ -145,7 +145,7 @@ describe('GddTransactionList', () => {
it('has a minus operator', () => { it('has a minus operator', () => {
expect(transaction.findAll('.gdd-transaction-list-item-operator').at(0).text()).toContain( expect(transaction.findAll('.gdd-transaction-list-item-operator').at(0).text()).toContain(
'-', '',
) )
}) })
@ -175,7 +175,7 @@ describe('GddTransactionList', () => {
it('shows the decay calculation', () => { it('shows the decay calculation', () => {
expect(transaction.findAll('div.gdd-transaction-list-item-decay').at(0).text()).toContain( expect(transaction.findAll('div.gdd-transaction-list-item-decay').at(0).text()).toContain(
'- 0.5', ' 0.5',
) )
}) })
}) })
@ -265,7 +265,7 @@ describe('GddTransactionList', () => {
it('shows the decay calculation', () => { it('shows the decay calculation', () => {
expect(transaction.findAll('.gdd-transaction-list-item-decay').at(0).text()).toContain( expect(transaction.findAll('.gdd-transaction-list-item-decay').at(0).text()).toContain(
'- 1.5', ' 1.5',
) )
}) })
}) })
@ -286,7 +286,7 @@ describe('GddTransactionList', () => {
it('has a minus operator', () => { it('has a minus operator', () => {
expect(transaction.findAll('.gdd-transaction-list-item-operator').at(0).text()).toContain( expect(transaction.findAll('.gdd-transaction-list-item-operator').at(0).text()).toContain(
'-', '',
) )
}) })

View File

@ -77,7 +77,7 @@
</b-col> </b-col>
<b-col cols="7"> <b-col cols="7">
<div class="gdd-transaction-list-item-date"> <div class="gdd-transaction-list-item-date">
{{ $d($moment(date), 'long') }} {{ $i18n.locale === 'de' ? 'Uhr' : '' }} {{ $d(new Date(date), 'long') }} {{ $i18n.locale === 'de' ? 'Uhr' : '' }}
</div> </div>
</b-col> </b-col>
</b-row> </b-row>
@ -146,10 +146,10 @@ import PaginationButtons from '../../../components/PaginationButtons'
import DecayInformation from '../../../components/DecayInformation' import DecayInformation from '../../../components/DecayInformation'
const iconsByType = { const iconsByType = {
send: { icon: 'arrow-left-circle', classes: 'text-danger', operator: '-' }, send: { icon: 'arrow-left-circle', classes: 'text-danger', operator: '' },
receive: { icon: 'arrow-right-circle', classes: 'gradido-global-color-accent', operator: '+' }, receive: { icon: 'arrow-right-circle', classes: 'gradido-global-color-accent', operator: '+' },
creation: { icon: 'gift', classes: 'gradido-global-color-accent', operator: '+' }, creation: { icon: 'gift', classes: 'gradido-global-color-accent', operator: '+' },
decay: { icon: 'droplet-half', classes: 'gradido-global-color-gray', operator: '-' }, decay: { icon: 'droplet-half', classes: 'gradido-global-color-gray', operator: '' },
} }
export default { export default {