Merge branch 'Elweyn/issue1558' into frontend-generate-link-for-send-gdd

This commit is contained in:
elweyn 2022-03-09 17:35:14 +01:00
commit c7bda9b15e
15 changed files with 322 additions and 1 deletions

View File

@ -18,6 +18,8 @@ export enum RIGHTS {
SET_PASSWORD = 'SET_PASSWORD', SET_PASSWORD = 'SET_PASSWORD',
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS', UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
HAS_ELOPAGE = 'HAS_ELOPAGE', HAS_ELOPAGE = 'HAS_ELOPAGE',
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
// Admin // Admin
SEARCH_USERS = 'SEARCH_USERS', SEARCH_USERS = 'SEARCH_USERS',
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION', CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',

View File

@ -18,6 +18,7 @@ export const ROLE_USER = new Role('user', [
RIGHTS.LOGOUT, RIGHTS.LOGOUT,
RIGHTS.UPDATE_USER_INFOS, RIGHTS.UPDATE_USER_INFOS,
RIGHTS.HAS_ELOPAGE, RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
]) ])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -10,7 +10,7 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0029-clean_transaction_table', DB_VERSION: '0030-transaction_link',
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

@ -0,0 +1,14 @@
import { ArgsType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ArgsType()
export default class TransactionLinkArgs {
@Field(() => Decimal)
amount: Decimal
@Field(() => String)
memo: string
@Field(() => Boolean, { nullable: true })
showEmail?: boolean
}

View File

@ -0,0 +1,50 @@
import { ObjectType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User } from './User'
@ObjectType()
export class TransactionLink {
constructor(transactionLink: dbTransactionLink, user: User) {
this.id = transactionLink.id
this.user = user
this.amount = transactionLink.amount
this.memo = transactionLink.memo
this.code = transactionLink.code
this.createdAt = transactionLink.createdAt
this.validUntil = transactionLink.validUntil
this.showEmail = transactionLink.showEmail
this.redeemedAt = null
this.redeemedBy = null
}
@Field(() => Number)
id: number
@Field(() => User)
user: User
@Field(() => Decimal)
amount: Decimal
@Field(() => String)
memo: string
@Field(() => String)
code: string
@Field(() => Date)
createdAt: Date
@Field(() => Date)
validUntil: Date
@Field(() => Boolean)
showEmail: boolean
@Field(() => Date, { nullable: true })
redeemedAt: Date | null
@Field(() => User, { nullable: true })
redeemedBy: User | null
}

View File

@ -0,0 +1,14 @@
import { transactionLinkCode } from './TransactionLinkResolver'
describe('transactionLinkCode', () => {
const date = new Date()
it('returns a string of length 96', () => {
expect(transactionLinkCode(date)).toHaveLength(96)
})
it('returns a string that ends with the hex value of date', () => {
const regexp = new RegExp(date.getTime().toString(16) + '$')
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
})
})

View File

@ -0,0 +1,75 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Resolver, Args, Authorized, Ctx, Mutation, Query, Arg } from 'type-graphql'
import { getCustomRepository } from '@dbTools/typeorm'
import { TransactionLink } from '@model/TransactionLink'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
import { UserRepository } from '@repository/User'
import { calculateBalance } from '@/util/validate'
import { RIGHTS } from '@/auth/RIGHTS'
import { randomBytes } from 'crypto'
import { User } from '@model/User'
// TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => {
const time = date.getTime().toString(16)
return (
randomBytes(48)
.toString('hex')
.substring(0, 96 - time.length) + time
)
}
const transactionLinkExpireDate = (date: Date): Date => {
// valid for 14 days
return new Date(date.setDate(date.getDate() + 14))
}
@Resolver()
export class TransactionLinkResolver {
@Authorized([RIGHTS.CREATE_TRANSACTION_LINK])
@Mutation(() => TransactionLink)
async createTransactionLink(
@Args() { amount, memo, showEmail = false }: TransactionLinkArgs,
@Ctx() context: any,
): Promise<TransactionLink> {
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findByPubkeyHex(context.pubKey)
// validate amount
// TODO taken from transaction resolver, duplicate code
const createdDate = new Date()
const sendBalance = await calculateBalance(user.id, amount.mul(-1), createdDate)
if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0")
}
// TODO!!!! Test balance for pending transaction links
const transactionLink = dbTransactionLink.create()
transactionLink.userId = user.id
transactionLink.amount = amount
transactionLink.memo = memo
transactionLink.code = transactionLinkCode(createdDate)
transactionLink.createdAt = createdDate
transactionLink.validUntil = transactionLinkExpireDate(createdDate)
transactionLink.showEmail = showEmail
await dbTransactionLink.save(transactionLink).catch((error) => {
throw error
})
return new TransactionLink(transactionLink, new User(user))
}
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
@Query(() => TransactionLink)
async queryTransactionLink(@Arg('code') code: string): Promise<TransactionLink> {
console.log(code)
const transactionLink = await dbTransactionLink.findOneOrFail({ code: code })
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findOneOrFail({ id: transactionLink.userId })
return new TransactionLink(transactionLink, new User(user))
}
}

View File

@ -0,0 +1,58 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
@Entity('transaction_links')
export class TransactionLink extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ unsigned: true, nullable: false })
userId: number
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ length: 96, nullable: false, collation: 'utf8mb4_unicode_ci' })
code: string
@Column({
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
createdAt: Date
@Column({
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
validUntil: Date
@Column({
type: 'boolean',
default: () => false,
nullable: false,
})
showEmail: boolean
@Column({
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: true,
})
redeemedAt?: Date | null
@Column({ type: 'int', unsigned: true, nullable: true })
redeemedBy?: number | null
}

View File

@ -0,0 +1 @@
export { TransactionLink } from './0030-transaction_link/TransactionLink'

View File

@ -3,6 +3,7 @@ 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 { TransactionLink } from './TransactionLink'
import { User } from './User' import { User } from './User'
import { UserSetting } from './UserSetting' import { UserSetting } from './UserSetting'
import { AdminPendingCreation } from './AdminPendingCreation' import { AdminPendingCreation } from './AdminPendingCreation'
@ -14,6 +15,7 @@ export const entities = [
Migration, Migration,
ServerUser, ServerUser,
Transaction, Transaction,
TransactionLink,
User, User,
UserSetting, UserSetting,
] ]

View File

@ -0,0 +1,26 @@
/* MIGRATION TO CREATE TRANSACTION_LINK TABLE */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE \`transaction_links\` (
\`id\` int UNSIGNED NOT NULL AUTO_INCREMENT,
\`userId\` int UNSIGNED NOT NULL,
\`amount\` DECIMAL(40,20) NOT NULL,
\`memo\` varchar(255) NOT NULL,
\`code\` varchar(96) NOT NULL,
\`createdAt\` datetime NOT NULL,
\`validUntil\` datetime NOT NULL,
\`showEmail\` boolean NOT NULL DEFAULT false,
\`redeemedAt\` datetime,
\`redeemedBy\` int UNSIGNED,
PRIMARY KEY (\`id\`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`DROP TABLE \`transaction_links\`;`)
}

View File

@ -127,3 +127,20 @@ export const communities = gql`
} }
} }
` `
export const queryTransactionLink = gql`
query($code: String!) {
queryTransactionLink(code: $code) {
amount
memo
createdAt
validUntil
user {
email
firstName
lastName
publisherId
}
}
}
`

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,56 @@
<template>
<div>
<!-- Header -->
<div class="header py-7 py-lg-8 pt-lg-9">
<b-container>
<div class="header-body text-center mb-7">
<p class="h1">
{{ displaySetup.user.firstName }} {{ displaySetup.user.lastName }}
{{ $t('wants to send you') }} {{ displaySetup.amount | GDD }}
</p>
<p class="h4">{{ $t(displaySetup.subtitle) }}</p>
<hr />
<b-button v-if="displaySetup.linkTo" :to="displaySetup.linkTo">
{{ $t(displaySetup.button) }}
</b-button>
</div>
</b-container>
</div>
</div>
</template>
<script>
import { queryTransactionLink } from '@/graphql/queries'
export default {
name: 'ShowTransactionLinkInformations',
data() {
return {
displaySetup: {},
}
},
methods: {
setTransactionLinkInformation() {
this.$apollo
.query({
query: queryTransactionLink,
variables: {
code: this.$route.params.code,
},
})
.then((result) => {
const {
data: { queryTransactionLink },
} = result
this.displaySetup = queryTransactionLink
this.$store.commit('publisherId', queryTransactionLink.user.publisherId)
})
.catch((error) => {
this.toastError(error)
})
},
},
created() {
this.setDisplaySetup()
},
}
</script>

View File

@ -82,6 +82,10 @@ const routes = [
path: '/checkEmail/:optin', path: '/checkEmail/:optin',
component: () => import('@/pages/ResetPassword.vue'), component: () => import('@/pages/ResetPassword.vue'),
}, },
{
path: '/redeem/:code',
component: () => import('@/pages/ShowTransactionLinkInformations.vue'),
},
{ path: '*', component: NotFound }, { path: '*', component: NotFound },
] ]