Merge branch 'master' into backend-tests-running-again

This commit is contained in:
Moriz Wahl 2022-02-21 13:24:42 +01:00
commit 970216b2fc
44 changed files with 603 additions and 412 deletions

View File

@ -3,7 +3,11 @@
<div class="shadow p-3 mb-5 bg-white rounded">
<div v-if="checked">{{ $t('unregister_mail.text_true') }}</div>
<div v-else>
{{ $t('unregister_mail.text_false', { date: dateLastSend, mail: email }) }}
{{
dateLastSend === ''
? $t('unregister_mail.never_sent', { email })
: $t('unregister_mail.text_false', { date: dateLastSend, email })
}}
<!-- Using components -->
<b-input-group :prepend="$t('unregister_mail.info')" class="mt-3">

View File

@ -1,15 +1,45 @@
<template>
<div class="">
<div>
<hr />
<br />
<div class="text-center">
{{ $t('gradido_admin_footer') }}
<div><small>Version: 1.0.0</small></div>
</div>
<b-row align-v="center" class="mt-4 justify-content-lg-between">
<b-col>
<div class="copyright text-center text-lg-center text-muted">
© {{ year }}
<a
:href="`https://gradido.net/${$i18n.locale}`"
class="font-weight-bold ml-1"
target="_blank"
>
{{ $t('gradido_admin_footer') }}
</a>
|
<a href="https://github.com/gradido/gradido/releases/latest" target="_blank">
App version {{ version }}
</a>
<a
v-if="hash"
:href="'https://github.com/gradido/gradido/commit/' + hash"
target="_blank"
>
({{ shortHash }})
</a>
</div>
</b-col>
</b-row>
</div>
</template>
<script>
import CONFIG from '../config'
export default {
name: 'ContentFooter',
data() {
return {
year: new Date().getFullYear(),
version: CONFIG.APP_VERSION,
hash: CONFIG.BUILD_COMMIT,
shortHash: CONFIG.BUILD_COMMIT_SHORT,
}
},
}
</script>

View File

@ -3,11 +3,15 @@ import NotFoundPage from './NotFoundPage'
const localVue = global.localVue
const mocks = {
$t: jest.fn((t) => t),
}
describe('NotFoundPage', () => {
let wrapper
const Wrapper = () => {
return mount(NotFoundPage, { localVue })
return mount(NotFoundPage, { localVue, mocks })
}
describe('mount', () => {
@ -18,5 +22,9 @@ describe('NotFoundPage', () => {
it('has a svg', () => {
expect(wrapper.find('svg').exists()).toBeTruthy()
})
it('has a back button', () => {
expect(wrapper.find('.test-back').exists()).toBeTruthy()
})
})
})

View File

@ -4,7 +4,7 @@
<div class="header py-1 py-lg-1 pt-lg-3">
<b-container>
<div class="header-body text-center mb-3">
<a href="login" to="login">
<a href="#!" @click="goback">
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-12 mt-5 mb-5">
@ -1185,6 +1185,11 @@
</div>
</b-container>
</div>
<div class="text-center">
<b-button class="test-back" variant="light" @click="goback">
{{ $t('back') }}
</b-button>
</div>
</div>
</template>
@ -1213,6 +1218,11 @@ export default {
},
}
},
methods: {
goback() {
this.$router.go(-1)
},
},
}
</script>
<style>

View File

@ -1,5 +1,6 @@
{
"all_emails": "Alle Nutzer",
"back": "zurück",
"bookmark": "bookmark",
"confirmed": "bestätigt",
"creation": "Schöpfung",
@ -75,8 +76,9 @@
"button": "Registrierungs-Email bestätigen, jetzt senden",
"error": "Fehler beim Senden des Bestätigungs-Links an den Benutzer: {message}",
"info": "Email bestätigen, wiederholt senden an:",
"never_sent": "Es scheint so, als ob wir nie eine E-Mail an {email} geschickt haben",
"success": "Erfolgreiches Senden des Bestätigungs-Links an die E-Mail des Nutzers! ({email})",
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({mail}) gesendet.",
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
"text_true": " Die Email wurde bestätigt."
},
"user_search": "Nutzer-Suche"

View File

@ -1,5 +1,6 @@
{
"all_emails": "All users",
"back": "back",
"bookmark": "Remember",
"confirmed": "confirmed",
"creation": "Creation",
@ -75,8 +76,9 @@
"button": "Confirm registration email, send now",
"error": "Error sending the confirmation link to the user: {message}",
"info": "Confirm email, send repeatedly to:",
"never_sent": "It seems we did never send an email to the member {email}",
"success": "Successfully send the confirmation link to the user's email! ({email})",
"text_false": "The last email was sent to the member ({mail}) on {date}.",
"text_false": "The last email was sent to the member ({email}) on {date}.",
"text_true": "The email was confirmed."
},
"user_search": "User search"

View File

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

View File

@ -8,21 +8,21 @@ import { PendingCreation } from '../model/PendingCreation'
import { CreatePendingCreations } from '../model/CreatePendingCreations'
import { UpdatePendingCreation } from '../model/UpdatePendingCreation'
import { RIGHTS } from '../../auth/RIGHTS'
import { TransactionRepository } from '../../typeorm/repository/Transaction'
import { UserRepository } from '../../typeorm/repository/User'
import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs'
import UpdatePendingCreationArgs from '../arg/UpdatePendingCreationArgs'
import SearchUsersArgs from '../arg/SearchUsersArgs'
import moment from 'moment'
import { Transaction } from '@entity/Transaction'
import { TransactionCreation } from '@entity/TransactionCreation'
import { UserTransaction } from '@entity/UserTransaction'
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
import { BalanceRepository } from '../../typeorm/repository/Balance'
import { calculateDecay } from '../../util/decay'
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
import { hasElopageBuys } from '../../util/hasElopageBuys'
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_UNKNOWN = 3 // elopage?
@ -82,8 +82,13 @@ export class AdminResolver {
async createPendingCreation(
@Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
): Promise<number[]> {
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findByEmail(email)
const user = await User.findOne({ email }, { withDeleted: true })
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) {
throw new Error('Creation could not be saved, Email is not activated')
}
@ -134,8 +139,13 @@ export class AdminResolver {
async updatePendingCreation(
@Args() { id, email, amount, memo, creationDate, moderator }: UpdatePendingCreationArgs,
): Promise<UpdatePendingCreation> {
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findByEmail(email)
const user = await User.findOne({ email }, { withDeleted: true })
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 })
@ -212,23 +222,17 @@ export class AdminResolver {
if (moderatorUser.id === pendingCreation.userId)
throw new Error('Moderator can not confirm own pending creation')
const transactionRepository = getCustomRepository(TransactionRepository)
const receivedCallDate = new Date()
let transaction = new Transaction()
transaction.transactionTypeId = 1
transaction.transactionTypeId = TransactionTypeId.CREATION
transaction.memo = pendingCreation.memo
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')
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 lastUserTransaction = await userTransactionRepository.findLastForUser(
pendingCreation.userId,
@ -256,15 +260,15 @@ export class AdminResolver {
throw new Error('Error saving user transaction: ' + error)
})
const balanceRepository = getCustomRepository(BalanceRepository)
let userBalance = await balanceRepository.findByUser(pendingCreation.userId)
if (!userBalance) userBalance = balanceRepository.create()
userBalance.userId = pendingCreation.userId
let userBalance = await Balance.findOne({ userId: pendingCreation.userId })
if (!userBalance) {
userBalance = new Balance()
userBalance.userId = pendingCreation.userId
}
userBalance.amount = Number(newBalance)
userBalance.modified = receivedCallDate
userBalance.recordDate = receivedCallDate
await balanceRepository.save(userBalance)
await userBalance.save()
await AdminPendingCreation.delete(pendingCreation)
return true
@ -278,12 +282,13 @@ async function getUserCreations(id: number): Promise<number[]> {
const lastMonthNumber = moment().subtract(1, 'month').format('M')
const currentMonthNumber = moment().format('M')
const createdAmountsQuery = await TransactionCreation.createQueryBuilder('transaction_creations')
.select('MONTH(transaction_creations.target_date)', 'target_month')
.addSelect('SUM(transaction_creations.amount)', 'sum')
.where('transaction_creations.state_user_id = :id', { id })
const createdAmountsQuery = await Transaction.createQueryBuilder('transactions')
.select('MONTH(transactions.creation_date)', 'target_month')
.addSelect('SUM(transactions.amount)', 'sum')
.where('transactions.user_id = :id', { id })
.andWhere('transactions.transaction_type_id = :type', { type: TransactionTypeId.CREATION })
.andWhere({
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
creationDate: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
date: dateBeforeLastMonth,
endDate: dateNextMonth,
}),

View File

@ -4,11 +4,11 @@
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { getCustomRepository } from '@dbTools/typeorm'
import { Balance } from '../model/Balance'
import { BalanceRepository } from '../../typeorm/repository/Balance'
import { UserRepository } from '../../typeorm/repository/User'
import { calculateDecay } from '../../util/decay'
import { roundFloorFrom4 } from '../../util/round'
import { RIGHTS } from '../../auth/RIGHTS'
import { Balance as dbBalance } from '@entity/Balance'
@Resolver()
export class BalanceResolver {
@ -16,11 +16,10 @@ export class BalanceResolver {
@Query(() => Balance)
async balance(@Ctx() context: any): Promise<Balance> {
// load user and balance
const balanceRepository = getCustomRepository(BalanceRepository)
const userRepository = getCustomRepository(UserRepository)
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()
// No balance found

View File

@ -3,7 +3,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
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 { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
@ -16,15 +16,12 @@ import Paginated from '../arg/Paginated'
import { Order } from '../enum/Order'
import { BalanceRepository } from '../../typeorm/repository/Balance'
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 { TransactionSendCoin as dbTransactionSendCoin } from '@entity/TransactionSendCoin'
import { Balance as dbBalance } from '@entity/Balance'
import { apiPost } from '../../apis/HttpRequest'
@ -50,15 +47,13 @@ async function calculateAndAddDecayTransactions(
transactionIds.push(userTransaction.transactionId)
})
const transactionRepository = getCustomRepository(TransactionRepository)
const transactions = await transactionRepository.joinFullTransactionsByIds(transactionIds)
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) {
involvedUserIds.push(transaction.transactionSendCoin.userId)
involvedUserIds.push(transaction.transactionSendCoin.recipiantUserId)
involvedUserIds.push(transaction.sendReceiverUserId!) // TODO ensure not null properly
}
})
// remove duplicates
@ -108,24 +103,21 @@ async function calculateAndAddDecayTransactions(
// balance
if (userTransaction.transactionTypeId === TransactionTypeId.CREATION) {
// creation
const creation = transaction.transactionCreation
finalTransaction.name = 'Gradido Akademie'
finalTransaction.type = TransactionType.CREATION
// finalTransaction.targetDate = creation.targetDate
finalTransaction.balance = roundFloorFrom4(creation.amount)
finalTransaction.balance = roundFloorFrom4(Number(transaction.amount)) // Todo unsafe conversion
} else if (userTransaction.transactionTypeId === TransactionTypeId.SEND) {
// send coin
const sendCoin = transaction.transactionSendCoin
let otherUser: dbUser | undefined
finalTransaction.balance = roundFloorFrom4(sendCoin.amount)
if (sendCoin.userId === user.id) {
finalTransaction.balance = roundFloorFrom4(Number(transaction.amount)) // Todo unsafe conversion
if (transaction.userId === user.id) {
finalTransaction.type = TransactionType.SEND
otherUser = userIndiced[sendCoin.recipiantUserId]
otherUser = userIndiced.find((u) => u.id === transaction.sendReceiverUserId)
// finalTransaction.pubkey = sendCoin.recipiantPublic
} else if (sendCoin.recipiantUserId === user.id) {
} else if (transaction.sendReceiverUserId === user.id) {
finalTransaction.type = TransactionType.RECIEVE
otherUser = userIndiced[sendCoin.userId]
otherUser = userIndiced.find((u) => u.id === transaction.userId)
// finalTransaction.pubkey = sendCoin.senderPublic
} else {
throw new Error('invalid transaction')
@ -153,61 +145,9 @@ async function calculateAndAddDecayTransactions(
finalTransactions.push(decayTransaction)
}
}
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
async function updateStateBalance(
user: dbUser,
@ -215,8 +155,7 @@ async function updateStateBalance(
received: Date,
queryRunner: QueryRunner,
): Promise<dbBalance> {
const balanceRepository = getCustomRepository(BalanceRepository)
let balance = await balanceRepository.findByUser(user.id)
let balance = await dbBalance.findOne({ userId: user.id })
if (!balance) {
balance = new dbBalance()
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()
export class TransactionResolver {
@Authorized([RIGHTS.TRANSACTION_LIST])
@ -299,43 +228,66 @@ export class TransactionResolver {
): Promise<TransactionList> {
// load user
const userRepository = getCustomRepository(UserRepository)
let userEntity: dbUser | undefined
if (userId) {
userEntity = await userRepository.findOneOrFail({ id: userId })
} else {
userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const user = userId
? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
: await userRepository.findByPubkeyHex(context.pubKey)
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)
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(
currentPage,
pageSize,
order,
userEntity,
onlyCreations,
)
const transactionList = new TransactionList()
transactionList.count = userTransactionsCount
transactionList.transactions = transactions
// get gdt sum
transactions.gdtSum = null
transactionList.gdtSum = null
try {
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) {}
// get balance
const balanceRepository = getCustomRepository(BalanceRepository)
const balanceEntity = await balanceRepository.findByUser(userEntity.id)
const balanceEntity = await dbBalance.findOne({ userId: user.id })
if (balanceEntity) {
const now = new Date()
transactions.balance = roundFloorFrom4(balanceEntity.amount)
transactions.decay = roundFloorFrom4(
transactionList.balance = roundFloorFrom4(balanceEntity.amount)
// TODO: Add a decay object here instead of static data representing the decay.
transactionList.decay = roundFloorFrom4(
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
)
transactions.decayDate = now.toString()
transactionList.decayDate = now.toString()
}
return transactions
return transactionList
}
@Authorized([RIGHTS.SEND_COINS])
@ -343,37 +295,28 @@ export class TransactionResolver {
async sendCoins(
@Args() { email, amount, memo }: TransactionSendArgs,
@Ctx() context: any,
): Promise<string> {
): Promise<boolean> {
// TODO this is subject to replay attacks
// validate sender user (logged in)
const userRepository = getCustomRepository(UserRepository)
const senderUser = await userRepository.findByPubkeyHex(context.pubKey)
if (senderUser.pubKey.length !== 32) {
throw new Error('invalid sender public key')
}
// validate 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
// TODO: the detour over the public key is unnecessary
const recipiantPublicKey = await getPublicKey(email)
if (!recipiantPublicKey) {
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
if (!recipientUser) {
throw new Error('recipient not known')
}
if (!isHexPublicKey(recipiantPublicKey)) {
throw new Error('invalid recipiant public key')
if (recipientUser.deletedAt) {
throw new Error('The recipient account was deleted')
}
const recipiantUser = await userRepository.findByPubkeyHex(recipiantPublicKey)
if (!recipiantUser) {
throw new Error('Cannot find recipiant user by local send coins transaction')
} else if (recipiantUser.disabled) {
throw new Error('recipiant user account is disabled')
}
// validate amount
if (amount <= 0) {
throw new Error('invalid amount')
if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) {
throw new Error('invalid recipient public key')
}
const centAmount = Math.trunc(amount * 10000)
@ -383,17 +326,16 @@ export class TransactionResolver {
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
// transaction
let transaction = new dbTransaction()
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)
// TODO: NO! this is problematic in its construction
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)
})
await queryRunner.manager.insert(dbTransaction, transaction)
// Insert Transaction: sender - amount
const senderUserTransactionBalance = await addUserTransaction(
@ -405,7 +347,7 @@ export class TransactionResolver {
// Insert Transaction: recipient + amount
const recipiantUserTransactionBalance = await addUserTransaction(
recipiantUser,
recipientUser,
transaction,
centAmount,
queryRunner,
@ -421,7 +363,7 @@ export class TransactionResolver {
// Update Balance: recipiant + amount
const recipiantStateBalance = await updateStateBalance(
recipiantUser,
recipientUser,
centAmount,
transaction.received,
queryRunner,
@ -434,18 +376,10 @@ export class TransactionResolver {
throw new Error('db data corrupted, recipiant')
}
// transactionSendCoin
const transactionSendCoin = new dbTransactionSendCoin()
transactionSendCoin.transactionId = transaction.id
transactionSendCoin.userId = senderUser.id
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)
})
// 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)
@ -474,13 +408,13 @@ export class TransactionResolver {
await sendTransactionReceivedEmail({
senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName,
recipientFirstName: recipiantUser.firstName,
recipientLastName: recipiantUser.lastName,
email: recipiantUser.email,
recipientFirstName: recipientUser.firstName,
recipientLastName: recipientUser.lastName,
email: recipientUser.email,
amount,
memo,
})
return 'success'
return true
}
}

View File

@ -3,7 +3,7 @@
import fs from 'fs'
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 { User } from '../model/User'
import { User as DbUser } from '@entity/User'
@ -152,8 +152,7 @@ const createEmailOptIn = async (
loginUserId: number,
queryRunner: QueryRunner,
): Promise<LoginEmailOptIn> => {
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
let emailOptIn = await loginEmailOptInRepository.findOne({
let emailOptIn = await LoginEmailOptIn.findOne({
userId: loginUserId,
emailOptInTypeId: EMAIL_OPT_IN_REGISTER,
})
@ -182,8 +181,7 @@ const createEmailOptIn = async (
}
const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
let optInCode = await loginEmailOptInRepository.findOne({
let optInCode = await LoginEmailOptIn.findOne({
userId: loginUserId,
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
})
@ -205,7 +203,7 @@ const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
optInCode.userId = loginUserId
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
}
await loginEmailOptInRepository.save(optInCode)
await LoginEmailOptIn.save(optInCode)
return optInCode
}
@ -250,9 +248,12 @@ export class UserResolver {
@Ctx() context: any,
): Promise<User> {
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')
})
if (dbUser.deletedAt) {
throw new Error('This user was permanently disabled. Contact support for questions.')
}
if (!dbUser.emailChecked) {
throw new Error('User email not validated')
}
@ -335,9 +336,9 @@ export class UserResolver {
// Validate email unique
// TODO: i can register an email in upper/lower case twice
const userRepository = getCustomRepository(UserRepository)
const usersFound = await userRepository.count({ email })
if (usersFound !== 0) {
// 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) {
// 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.`)
}
@ -487,12 +488,9 @@ export class UserResolver {
}
// Load code
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
const optInCode = await loginEmailOptInRepository
.findOneOrFail({ verificationCode: code })
.catch(() => {
throw new Error('Could not login with emailVerificationCode')
})
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
throw new Error('Could not login with emailVerificationCode')
})
// Code is only valid for 10minutes
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()

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

@ -9,30 +9,17 @@ export class UserRepository extends Repository<User> {
.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[]> {
if (!userIds.length) return []
const users = await this.createQueryBuilder('user')
return 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'])
.where('user.id IN (:...users)', { users: userIds })
.where('user.id IN (:...userIds)', { userIds })
.getMany()
const usersIndiced: User[] = []
users.forEach((value) => {
usersIndiced[value.id] = value
})
return usersIndiced
}
async findBySearchCriteria(searchCriteria: string): Promise<User[]> {
return await this.createQueryBuilder('user')
return this.createQueryBuilder('user')
.withDeleted()
.where(
'user.firstName like :name or user.lastName like :lastName or user.email like :email',
{

View File

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

View File

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

View File

@ -0,0 +1,75 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
DeleteDateColumn,
} from 'typeorm'
import { UserSetting } from '../UserSetting'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
privKey: Buffer
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@DeleteDateColumn()
deletedAt: Date | null
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@Column({
type: 'text',
name: 'passphrase',
collation: 'utf8mb4_unicode_ci',
nullable: true,
default: null,
})
passphrase: string
@OneToMany(() => UserSetting, (userSetting) => userSetting.user)
settings: UserSetting[]
}

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

@ -1 +1 @@
export { User } from './0020-rename_and_clean_state_users/User'
export { User } from './0023-users_disabled_soft_delete/User'

View File

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

View File

@ -0,0 +1,26 @@
/* MIGRATION TO IMPLEMENT SOFT DELETE ON THE USERS TABLE
*
* Replace the `disabled` column with `deletedAt` containing
* a date as it is standard for soft delete fields
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Create new `deletedAt` column
await queryFn(
'ALTER TABLE `users` ADD COLUMN `deletedAt` datetime DEFAULT NULL AFTER `disabled`;',
)
// Insert a 1.1.2022 as date for those users with `disabled=1`
await queryFn('UPDATE `users` SET `deletedAt` = "2022-01-01 00:00:00" WHERE `disabled` = 1;')
// Delete `disabled` column
await queryFn('ALTER TABLE `users` DROP COLUMN `disabled`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `users` ADD COLUMN `disabled` tinyint(4) NOT NULL DEFAULT 0 AFTER `deletedAt`;',
)
await queryFn('UPDATE `users` SET `disabled` = 1 WHERE `deletedAt` IS NOT NULL;')
await queryFn('ALTER TABLE `users` DROP COLUMN `deletedAt`;')
}

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

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

View File

@ -1,7 +1,7 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { User } from '../../entity/User'
import { randomBytes, randomInt } from 'crypto'
import { randomBytes } from 'crypto'
import { UserContext } from '../interface/UserContext'
define(User, (faker: typeof Faker, context?: UserContext) => {
@ -12,7 +12,7 @@ define(User, (faker: typeof Faker, context?: UserContext) => {
user.email = context.email ? context.email : faker.internet.email()
user.firstName = context.firstName ? context.firstName : faker.name.firstName()
user.lastName = context.lastName ? context.lastName : faker.name.lastName()
user.disabled = context.disabled ? context.disabled : false
user.deletedAt = context.deletedAt ? context.deletedAt : null
// TODO Create real password and keys/hash
user.password = context.password ? context.password : BigInt(0)
user.privKey = context.privKey ? context.privKey : randomBytes(80)

View File

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

View File

@ -3,7 +3,7 @@ export interface UserContext {
email?: string
firstName?: string
lastName?: string
disabled?: boolean
deletedAt?: Date
password?: BigInt
privKey?: Buffer
emailHash?: Buffer

View File

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

View File

@ -2,7 +2,6 @@ import { UserContext, ServerUserContext } from '../../interface/UserContext'
import {
BalanceContext,
TransactionContext,
TransactionCreationContext,
UserTransactionContext,
} from '../../interface/TransactionContext'
import { UserInterface } from '../../interface/UserInterface'
@ -11,7 +10,6 @@ import { ServerUser } from '../../../entity/ServerUser'
import { Balance } from '../../../entity/Balance'
import { Transaction } from '../../../entity/Transaction'
import { UserTransaction } from '../../../entity/UserTransaction'
import { TransactionCreation } from '../../../entity/TransactionCreation'
import { Factory } from 'typeorm-seeding'
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
await factory(Balance)(createBalanceContext(userData, user)).create()
const transaction = await factory(Transaction)(
createTransactionContext(userData, 1, 'Herzlich Willkommen bei Gradido!'),
).create()
await factory(TransactionCreation)(
createTransactionCreationContext(userData, user, transaction),
createTransactionContext(userData, user, 1, 'Herzlich Willkommen bei Gradido!'),
).create()
await factory(UserTransaction)(
createUserTransactionContext(userData, user, transaction),
@ -42,7 +37,7 @@ const createUserContext = (context: UserInterface): UserContext => {
email: context.email,
firstName: context.firstName,
lastName: context.lastName,
disabled: context.disabled,
deletedAt: context.deletedAt,
password: context.password,
privKey: context.privKey,
emailHash: context.emailHash,
@ -76,27 +71,18 @@ const createBalanceContext = (context: UserInterface, user: User): BalanceContex
const createTransactionContext = (
context: UserInterface,
user: User,
type: number,
memo: string,
): TransactionContext => {
return {
transactionTypeId: type,
userId: user.id,
amount: BigInt(context.amount || 100000),
txHash: context.creationTxHash,
memo,
received: context.recordDate,
}
}
const createTransactionCreationContext = (
context: UserInterface,
user: User,
transaction: Transaction,
): TransactionCreationContext => {
return {
userId: user.id,
amount: context.amount,
targetDate: context.targetDate,
transaction,
creationDate: context.creationDate,
}
}
@ -112,6 +98,6 @@ const createUserTransactionContext = (
balance: context.amount,
balanceDate: context.recordDate,
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',
firstName: 'Bibi',
lastName: 'Bloxberg',
username: 'bibi',
// description: 'Hex Hex',
password: BigInt('12825419584724616625'),
pubKey: Buffer.from('42de7e4754625b730018c3b4ea745a4d043d9d867af352d0f08871793dfa6743', 'hex'),
@ -14,16 +15,13 @@ export const bibiBloxberg = {
createdAt: new Date('2021-11-26T11:32:16'),
emailChecked: true,
language: 'de',
disabled: false,
groupId: 1,
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 ',
mnemonicType: 2,
isAdmin: false,
addBalance: true,
balanceModified: 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,
creationTxHash: Buffer.from(
'51103dc0fc2ca5d5d75a9557a1e899304e5406cfdb1328d8df6414d527b0118100000000000000000000000000000000',
@ -33,8 +31,4 @@ export const bibiBloxberg = {
'2a2c71f3e41adc060bbc3086577e2d57d24eeeb0a7727339c3f85aad813808f601d7e1df56a26e0929d2e67fc054fca429ccfa283ed2782185c7f009fe008f0c',
'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',
firstName: 'Bob',
lastName: 'der Baumeister',
username: 'bob',
// description: 'Können wir das schaffen? Ja, wir schaffen das!',
password: BigInt('3296644341468822636'),
pubKey: Buffer.from('a509d9a146374fc975e3677db801ae8a4a83bff9dea96da64053ff6de6b2dd7e', 'hex'),
@ -14,16 +15,13 @@ export const bobBaumeister = {
createdAt: new Date('2021-11-26T11:36:31'),
emailChecked: true,
language: 'de',
disabled: false,
groupId: 1,
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 ',
mnemonicType: 2,
isAdmin: false,
addBalance: true,
balanceModified: 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,
creationTxHash: Buffer.from(
'be095dc87acb94987e71168fee8ecbf50ecb43a180b1006e75d573b35725c69c00000000000000000000000000000000',
@ -33,8 +31,4 @@ export const bobBaumeister = {
'1fbd6b9a3d359923b2501557f3bc79fa7e428127c8090fb16bc490b4d87870ab142b3817ddd902d22f0b26472a483233784a0e460c0622661752a13978903905',
'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',
firstName: 'Garrick',
lastName: 'Ollivander',
username: 'garrick',
// 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.`,
password: BigInt('0'),
@ -10,11 +11,8 @@ export const garrickOllivander = {
createdAt: new Date('2022-01-10T10:23:17'),
emailChecked: false,
language: 'en',
disabled: false,
groupId: 1,
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 ',
mnemonicType: 2,
isAdmin: 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',
firstName: 'Peter',
lastName: 'Lustig',
username: 'peter',
// description: 'Latzhose und Nickelbrille',
password: BigInt('3917921995996627700'),
pubKey: Buffer.from('7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770', 'hex'),
@ -14,11 +15,8 @@ export const peterLustig = {
createdAt: new Date('2020-11-25T10:48:43'),
emailChecked: true,
language: 'de',
disabled: false,
groupId: 1,
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 ',
mnemonicType: 2,
role: 'admin',
serverUserPassword: '$2y$10$TzIWLeZoKs251gwrhSQmHeKhKI/EQ4EV5ClfAT8Ufnb4lcUXPa5X.',
activated: 1,

View File

@ -1,8 +1,9 @@
export const raeuberHotzenplotz = {
import { UserInterface } from '../../interface/UserInterface'
export const raeuberHotzenplotz: UserInterface = {
email: 'raeuber@hotzenplotz.de',
firstName: 'Räuber',
lastName: 'Hotzenplotz',
username: 'räuber',
// description: 'Pfefferpistole',
password: BigInt('12123692783243004812'),
pubKey: Buffer.from('d7c70f94234dff071d982aa8f41583876c356599773b5911b39080da2b8c2d2b', 'hex'),
@ -14,16 +15,13 @@ export const raeuberHotzenplotz = {
createdAt: new Date('2021-11-26T11:32:16'),
emailChecked: true,
language: 'de',
disabled: false,
groupId: 1,
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 ',
mnemonicType: 2,
isAdmin: false,
addBalance: true,
balanceModified: 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,
creationTxHash: Buffer.from(
'23ba44fd84deb59b9f32969ad0cb18bfa4588be1bdb99c396888506474c16c1900000000000000000000000000000000',
@ -33,8 +31,4 @@ export const raeuberHotzenplotz = {
'756d3da061687c575d1dbc5073908f646aa5f498b0927b217c83b48af471450e571dfe8421fb8e1f1ebd1104526b7e7c6fa78684e2da59c8f7f5a8dc3d9e5b0b',
'hex',
),
signaturePubkey: Buffer.from(
'7281e0ee3258b08801f3ec73e431b4519677f65c03b0382c63a913b5784ee770',
'hex',
),
}

View File

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

View File

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

View File

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

View File

@ -3,11 +3,15 @@ import NotFoundPage from './NotFoundPage'
const localVue = global.localVue
const mocks = {
$t: jest.fn((t) => t),
}
describe('NotFoundPage', () => {
let wrapper
const Wrapper = () => {
return mount(NotFoundPage, { localVue })
return mount(NotFoundPage, { localVue, mocks })
}
describe('mount', () => {
@ -18,5 +22,9 @@ describe('NotFoundPage', () => {
it('has a svg', () => {
expect(wrapper.find('svg').exists()).toBeTruthy()
})
it('has a back button', () => {
expect(wrapper.find('.test-back').exists()).toBeTruthy()
})
})
})

View File

@ -4,7 +4,7 @@
<div class="header py-1 py-lg-1 pt-lg-3">
<b-container>
<div class="header-body text-center mb-3">
<a href="login" to="login">
<a href="#!" @click="goback">
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-12 mt-5 mb-5">
@ -1185,6 +1185,11 @@
</div>
</b-container>
</div>
<div class="text-center">
<b-button class="test-back" variant="light" @click="goback">
{{ $t('back') }}
</b-button>
</div>
</div>
</template>
@ -1213,6 +1218,11 @@ export default {
},
}
},
methods: {
goback() {
this.$router.go(-1)
},
},
}
</script>
<style>

View File

@ -145,7 +145,7 @@ describe('GddTransactionList', () => {
it('has a minus operator', () => {
expect(transaction.findAll('.gdd-transaction-list-item-operator').at(0).text()).toContain(
'-',
'',
)
})
@ -175,7 +175,7 @@ describe('GddTransactionList', () => {
it('shows the decay calculation', () => {
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', () => {
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', () => {
expect(transaction.findAll('.gdd-transaction-list-item-operator').at(0).text()).toContain(
'-',
'',
)
})

View File

@ -77,7 +77,7 @@
</b-col>
<b-col cols="7">
<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>
</b-col>
</b-row>
@ -146,10 +146,10 @@ import PaginationButtons from '../../../components/PaginationButtons'
import DecayInformation from '../../../components/DecayInformation'
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: '+' },
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 {