From 0bf9843c1f0847df0c204264d462286da0038d59 Mon Sep 17 00:00:00 2001 From: embr Date: Wed, 8 Feb 2023 22:21:25 +0100 Subject: [PATCH] AfRApay.Web: Make transactions idempotent First rule of networking is the network is unreliable. Sometimes things get lost, sometimes it gets found multiple times for TCP reasons or because a browser tries to be clever. And when you're dealing with money, even if it's monopoly money, you don't want a duplicated request to mean a double-debit. The easiest way to do this is to simply include an idempotency key with each request - if that key is repeated, the request is ignored. --- AfRApay.FTM/Program.cs | 5 ++++- AfRApay.Web/Backend/Tables/User.cs | 7 ++++--- AfRApay.Web/Controllers/CardTransaction.cs | 19 ++++++++++++------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/AfRApay.FTM/Program.cs b/AfRApay.FTM/Program.cs index 9bbd8bb..5b63610 100644 --- a/AfRApay.FTM/Program.cs +++ b/AfRApay.FTM/Program.cs @@ -147,7 +147,10 @@ static async void HandleTap(IsoReader reader, HttpClient httpClient, TerminalSta case TerminalState.Debit: case TerminalState.Credit: var finalAmount = Math.Abs(amount) * (state == TerminalState.Debit ? 1 : -1); - await CallGet(httpClient, String.Format("/api/card/transaction?card={0}&amount={1}", Convert.ToHexString(uid), finalAmount)); + var idempotencyKey = new byte[18]; + Random.Shared.NextBytes(idempotencyKey); + await CallGet(httpClient, String.Format("/api/card/transaction?card={0}&amount={1}&ik={2}", + Convert.ToHexString(uid), finalAmount, Convert.ToBase64String(idempotencyKey))); break; case TerminalState.Link: await CallGet(httpClient, String.Format("/api/card/link?card={0}", Convert.ToHexString(uid))); diff --git a/AfRApay.Web/Backend/Tables/User.cs b/AfRApay.Web/Backend/Tables/User.cs index 1526b59..e2818c1 100644 --- a/AfRApay.Web/Backend/Tables/User.cs +++ b/AfRApay.Web/Backend/Tables/User.cs @@ -6,7 +6,8 @@ namespace AfRApay.Web.Backend.Tables; [Table(Name = "Users")] public class User { - [Column(Name = "ID"), PrimaryKey, Identity, NotNull] public int Id { get; set; } - [Column(Name = "Nickname"), NotNull] public string Nickname { get; set; } - [Column(Name = "Balance")] public decimal Balance { get; set; } + [Column(Name = "ID"), PrimaryKey, Identity, NotNull] public int Id { get; set; } + [Column(Name = "Nickname"), NotNull] public string Nickname { get; set; } + [Column(Name = "Balance")] public decimal Balance { get; set; } + [Column(Name = "LastIdempotencyKey")] public string LastIdempotencyKey { get; set; } } diff --git a/AfRApay.Web/Controllers/CardTransaction.cs b/AfRApay.Web/Controllers/CardTransaction.cs index 0f73b57..15f6a05 100644 --- a/AfRApay.Web/Controllers/CardTransaction.cs +++ b/AfRApay.Web/Controllers/CardTransaction.cs @@ -7,18 +7,23 @@ namespace AfRApay.Web.Controllers; [ApiController, Route("/api/card/transaction")] public class CardTransaction : Controller { [HttpGet] - public string Get([FromQuery] string card, [FromQuery] decimal amount) { + public string Get([FromQuery] string card, [FromQuery] decimal amount, [FromQuery] string ik) { var db = new Database.DbConn(); if (db.Cards.Any(p => p.CardId == card)) { var userId = db.Cards.First(p => p.CardId == card).UserId; var user = db.Users.First(p => p.Id == userId); - - if (user.Balance - amount < -50) { - return "E:Balance too low!"; + + // If an idempotency key is given, disregard duplicate requests. + if (ik == "" || ik != user.LastIdempotencyKey) { + if (user.Balance - amount < -50) { + return "E:Balance too low!"; + } + + user.Balance -= amount; + user.LastIdempotencyKey = ik; + db.Update(user); } - - user.Balance -= amount; - db.Update(user); + return $"S:{user.Nickname}: {user.Balance:N2}"; }