From 77b7d5b414d853ea96841c0db5ba65956ca91321 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 29 Jun 2023 12:09:07 +0200 Subject: [PATCH 1/4] Expect to receive an error on second call. --- backend/src/graphql/resolver/semaphore.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/semaphore.test.ts b/backend/src/graphql/resolver/semaphore.test.ts index eb4052e1f..a3624f189 100644 --- a/backend/src/graphql/resolver/semaphore.test.ts +++ b/backend/src/graphql/resolver/semaphore.test.ts @@ -4,6 +4,7 @@ import { Connection } from '@dbTools/typeorm' import { ApolloServerTestClient } from 'apollo-server-testing' import { Decimal } from 'decimal.js-light' +import { GraphQLError } from 'graphql' import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers' @@ -219,7 +220,7 @@ describe('semaphore', () => { }) }) - it('does not throw, but should', async () => { + it('does throw error on second redeem call', async () => { const redeem1 = mutate({ mutation: redeemTransactionLink, variables: { @@ -236,7 +237,7 @@ describe('semaphore', () => { errors: undefined, }) await expect(redeem2).resolves.toMatchObject({ - errors: undefined, + errors: [new GraphQLError('Transaction link already redeemed')], }) }) }) From 74ce9b3067f7a6261a92c6fc45ed544f8bfaa166 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 29 Jun 2023 12:09:42 +0200 Subject: [PATCH 2/4] Create a semafore lock for transaction links. --- backend/src/util/TRANSACTION_LINK_LOCK.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 backend/src/util/TRANSACTION_LINK_LOCK.ts diff --git a/backend/src/util/TRANSACTION_LINK_LOCK.ts b/backend/src/util/TRANSACTION_LINK_LOCK.ts new file mode 100644 index 000000000..8058d9a81 --- /dev/null +++ b/backend/src/util/TRANSACTION_LINK_LOCK.ts @@ -0,0 +1,4 @@ +import { Semaphore } from 'await-semaphore' + +const CONCURRENT_TRANSACTIONS = 1 +export const TRANSACTION_LINK_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS) From 8c9b4614b2d480c2e599d4f175ad7de25b412ae8 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 29 Jun 2023 12:12:49 +0200 Subject: [PATCH 3/4] Surrond the check if link is correct with the new semafore. --- .../resolver/TransactionLinkResolver.ts | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index d6649814a..a2cdabd81 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -33,6 +33,7 @@ import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { calculateDecay } from '@/util/decay' +import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' @@ -302,49 +303,49 @@ export class TransactionLinkResolver { return true } else { const now = new Date() - const transactionLink = await DbTransactionLink.findOne({ code }) - if (!transactionLink) { - throw new LogError('Transaction link not found', code) + const releaseLinkLock = await TRANSACTION_LINK_LOCK.acquire() + try { + const transactionLink = await DbTransactionLink.findOne({ code }) + if (!transactionLink) { + throw new LogError('Transaction link not found', code) + } + + const linkedUser = await DbUser.findOne( + { id: transactionLink.userId }, + { relations: ['emailContact'] }, + ) + + if (!linkedUser) { + throw new LogError('Linked user not found for given link', transactionLink.userId) + } + + if (user.id === linkedUser.id) { + throw new LogError('Cannot redeem own transaction link', user.id) + } + + if (transactionLink.validUntil.getTime() < now.getTime()) { + throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil) + } + + if (transactionLink.redeemedBy) { + throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy) + } + await executeTransaction( + transactionLink.amount, + transactionLink.memo, + linkedUser, + user, + transactionLink, + ) + await EVENT_TRANSACTION_LINK_REDEEM( + user, + { id: transactionLink.userId } as DbUser, + transactionLink, + transactionLink.amount, + ) + } finally { + releaseLinkLock() } - - const linkedUser = await DbUser.findOne( - { id: transactionLink.userId }, - { relations: ['emailContact'] }, - ) - - if (!linkedUser) { - throw new LogError('Linked user not found for given link', transactionLink.userId) - } - - if (user.id === linkedUser.id) { - throw new LogError('Cannot redeem own transaction link', user.id) - } - - // TODO: The now check should be done within the semaphore lock, - // since the program might wait a while till it is ready to proceed - // writing the transaction. - if (transactionLink.validUntil.getTime() < now.getTime()) { - throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil) - } - - if (transactionLink.redeemedBy) { - throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy) - } - - await executeTransaction( - transactionLink.amount, - transactionLink.memo, - linkedUser, - user, - transactionLink, - ) - await EVENT_TRANSACTION_LINK_REDEEM( - user, - { id: transactionLink.userId } as DbUser, - transactionLink, - transactionLink.amount, - ) - return true } } From db74bf07781532ef1abbb51c6f370bbe87a2ea22 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 30 Jun 2023 11:44:19 +0200 Subject: [PATCH 4/4] Fix error that resulted of merge conflict --- backend/src/graphql/resolver/TransactionLinkResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index d66755c51..282e6662a 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -311,7 +311,7 @@ export class TransactionLinkResolver { const now = new Date() const releaseLinkLock = await TRANSACTION_LINK_LOCK.acquire() try { - const transactionLink = await DbTransactionLink.findOne({ code }) + const transactionLink = await DbTransactionLink.findOne({ where: { code } }) if (!transactionLink) { throw new LogError('Transaction link not found', code) }