From 9bb80f6dce36a41b096326b671e3c875ae86eedc Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Tue, 23 Aug 2022 21:23:21 +0000 Subject: [PATCH] Make protocoin transactions atomic to fix race conditions --- apiserver/apiserver/api/views.py | 230 ++++++++++++++++--------------- 1 file changed, 119 insertions(+), 111 deletions(-) diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index 518d3e8..c661646 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -1047,85 +1047,89 @@ class InterestViewSet(Base, Retrieve, Create): class ProtocoinViewSet(Base): @action(detail=False, methods=['post'], permission_classes=[AllowMetadata | IsAuthenticated]) def send_to_member(self, request): - source_user = self.request.user - source_member = source_user.member - try: - member_id = int(request.data['member_id']) - except KeyError: - raise exceptions.ValidationError(dict(member_id='This field is required.')) - except ValueError: - raise exceptions.ValidationError(dict(member_id='Invalid number.')) + with transaction.atomic(): + source_user = self.request.user + source_member = source_user.member - try: - balance = float(request.data['balance']) - except KeyError: - raise exceptions.ValidationError(dict(balance='This field is required.')) - except ValueError: - raise exceptions.ValidationError(dict(balance='Invalid number.')) + try: + member_id = int(request.data['member_id']) + except KeyError: + raise exceptions.ValidationError(dict(member_id='This field is required.')) + except ValueError: + raise exceptions.ValidationError(dict(member_id='Invalid number.')) - try: - amount = float(request.data['amount']) - except KeyError: - raise exceptions.ValidationError(dict(amount='This field is required.')) - except ValueError: - raise exceptions.ValidationError(dict(amount='Invalid number.')) + try: + balance = float(request.data['balance']) + except KeyError: + raise exceptions.ValidationError(dict(balance='This field is required.')) + except ValueError: + raise exceptions.ValidationError(dict(balance='Invalid number.')) - if amount < 1.00: - raise exceptions.ValidationError(dict(amount='Amount too small.')) + try: + amount = float(request.data['amount']) + except KeyError: + raise exceptions.ValidationError(dict(amount='This field is required.')) + except ValueError: + raise exceptions.ValidationError(dict(amount='Invalid number.')) + + if amount < 1.00: + raise exceptions.ValidationError(dict(amount='Amount too small.')) - if member_id == source_member.id: - raise exceptions.ValidationError(dict(member_id='Unable to send to self.')) + if member_id == source_member.id: + raise exceptions.ValidationError(dict(member_id='Unable to send to self.')) - destination_member = get_object_or_404(models.Member, id=member_id) - destination_user = destination_member.user + destination_member = get_object_or_404(models.Member, id=member_id) + destination_user = destination_member.user - source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] - source_user_balance = float(source_user_balance) + source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] + source_user_balance = float(source_user_balance) - if source_user_balance != balance: - raise exceptions.ValidationError(dict(balance='Incorrect current balance.')) + if source_user_balance != balance: + raise exceptions.ValidationError(dict(balance='Incorrect current balance.')) - if source_user_balance < amount: - raise exceptions.ValidationError(dict(amount='Insufficient funds.')) + if source_user_balance < amount: + raise exceptions.ValidationError(dict(amount='Insufficient funds.')) - source_delta = -amount - destination_delta = amount + source_delta = -amount + destination_delta = amount - memo = 'Protocoin - Transaction {} ({}) sent ₱ {} to {} ({})'.format( - source_member.first_name + ' ' + source_member.last_name, - source_member.id, - amount, - destination_member.first_name + ' ' + destination_member.last_name, - destination_member.id, - ) + memo = 'Protocoin - Transaction {} ({}) sent ₱ {} to {} ({})'.format( + source_member.first_name + ' ' + source_member.last_name, + source_member.id, + amount, + destination_member.first_name + ' ' + destination_member.last_name, + destination_member.id, + ) - tx = models.Transaction.objects.create( - user=source_user, - protocoin=source_delta, - amount=0, - number_of_membership_months=0, - account_type='Protocoin', - category='Other', - info_source='System', - memo=memo, - ) - utils.log_transaction(tx) + tx = models.Transaction.objects.create( + user=source_user, + protocoin=source_delta, + amount=0, + number_of_membership_months=0, + account_type='Protocoin', + category='Other', + info_source='System', + memo=memo, + ) + utils.log_transaction(tx) - tx = models.Transaction.objects.create( - user=destination_user, - protocoin=destination_delta, - amount=0, - number_of_membership_months=0, - account_type='Protocoin', - category='Other', - info_source='System', - memo=memo, - ) - utils.log_transaction(tx) + tx = models.Transaction.objects.create( + user=destination_user, + protocoin=destination_delta, + amount=0, + number_of_membership_months=0, + account_type='Protocoin', + category='Other', + info_source='System', + memo=memo, + ) + utils.log_transaction(tx) - return Response(200) + return Response(200) + except OperationalError: + self.send_to_member(request) @action(detail=True, methods=['get']) def card_vend_balance(self, request, pk=None): @@ -1147,65 +1151,69 @@ class ProtocoinViewSet(Base): @action(detail=True, methods=['post']) def card_vend_request(self, request, pk=None): - auth_token = request.META.get('HTTP_AUTHORIZATION', '') - if secrets.VEND_API_TOKEN and auth_token != 'Bearer ' + secrets.VEND_API_TOKEN: - raise exceptions.PermissionDenied() - - source_card = get_object_or_404(models.Card, card_number=pk) - source_user = source_card.user - try: - number = request.data['number'] - except KeyError: - raise exceptions.ValidationError(dict(number='This field is required.')) + with transaction.atomic(): + auth_token = request.META.get('HTTP_AUTHORIZATION', '') + if secrets.VEND_API_TOKEN and auth_token != 'Bearer ' + secrets.VEND_API_TOKEN: + raise exceptions.PermissionDenied() - try: - balance = float(request.data['balance']) - except KeyError: - raise exceptions.ValidationError(dict(balance='This field is required.')) - except ValueError: - raise exceptions.ValidationError(dict(balance='Invalid number.')) + source_card = get_object_or_404(models.Card, card_number=pk) + source_user = source_card.user - try: - amount = float(request.data['amount']) - except KeyError: - raise exceptions.ValidationError(dict(amount='This field is required.')) - except ValueError: - raise exceptions.ValidationError(dict(amount='Invalid number.')) + try: + number = request.data['number'] + except KeyError: + raise exceptions.ValidationError(dict(number='This field is required.')) - if amount < 1.00: - raise exceptions.ValidationError(dict(amount='Amount too small.')) + try: + balance = float(request.data['balance']) + except KeyError: + raise exceptions.ValidationError(dict(balance='This field is required.')) + except ValueError: + raise exceptions.ValidationError(dict(balance='Invalid number.')) + + try: + amount = float(request.data['amount']) + except KeyError: + raise exceptions.ValidationError(dict(amount='This field is required.')) + except ValueError: + raise exceptions.ValidationError(dict(amount='Invalid number.')) + + if amount < 1.00: + raise exceptions.ValidationError(dict(amount='Amount too small.')) - source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] - source_user_balance = float(source_user_balance) + source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] + source_user_balance = float(source_user_balance) - if source_user_balance != balance: - raise exceptions.ValidationError(dict(balance='Incorrect current balance.')) + if source_user_balance != balance: + raise exceptions.ValidationError(dict(balance='Incorrect current balance.')) - if source_user_balance < amount: - raise exceptions.ValidationError(dict(amount='Insufficient funds.')) + if source_user_balance < amount: + raise exceptions.ValidationError(dict(amount='Insufficient funds.')) - source_delta = -amount + source_delta = -amount - memo = 'Protocoin - Purchase spent ₱ {} on vending machine item #{}'.format( - amount, - number, - ) + memo = 'Protocoin - Purchase spent ₱ {} on vending machine item #{}'.format( + amount, + number, + ) - tx = models.Transaction.objects.create( - user=source_user, - protocoin=source_delta, - amount=0, - number_of_membership_months=0, - account_type='Protocoin', - category='Snacks', - info_source='System', - memo=memo, - ) - utils.log_transaction(tx) + tx = models.Transaction.objects.create( + user=source_user, + protocoin=source_delta, + amount=0, + number_of_membership_months=0, + account_type='Protocoin', + category='Snacks', + info_source='System', + memo=memo, + ) + utils.log_transaction(tx) - return Response(200) + return Response(200) + except OperationalError: + self.card_vend_request(request, pk) class RegistrationView(RegisterView):