using AfRApay.Web.Backend.Database; using AfRApay.Web.Backend.Database.Tables; using AfRApay.Web.Controllers.Schema; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Swashbuckle.AspNetCore.Filters; namespace AfRApay.Web.Controllers; [ApiController] public class CardController : Controller { /// /// Links the given card to the user currently in link mode. /// /// The ID of the card /// Type of reader that scanned the card /// Returns 200 if the link succeeded /// Returns 304 if the card was already linked /// Returns 404 if no link process is active [HttpPut] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))] [ProducesResponseType(StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] [SwaggerResponseExample(StatusCodes.Status200OK, typeof(UserUpdatedExample))] [SwaggerResponseExample(StatusCodes.Status404NotFound, typeof(ErrorNoActiveLinkProcessExample))] [Route("/api/card/{card}/link")] public async Task Link(string card, [FromQuery] string? reader) { var db = new DatabaseContext(); if (db.Cards.Any(p => p.Id == card)) { return StatusCode(StatusCodes.Status304NotModified); } var linkFlag = db.Config.FirstOrDefault(p => p.Name == "link"); if (string.IsNullOrWhiteSpace(linkFlag?.Value)) { return NotFound(new ErrorResponse("No active link process")); } var lTimeFlag = db.Config.FirstOrDefault(p => p.Name == "lTime"); if (string.IsNullOrWhiteSpace(lTimeFlag?.Value)) { return NotFound(new ErrorResponse("No active link process")); } if (DateTime.UtcNow - DateTime.Parse(lTimeFlag.Value) > TimeSpan.FromMinutes(5)) { return NotFound(new ErrorResponse("No active link process")); } var user = db.Users.First(p => p.Id == int.Parse(linkFlag.Value)); linkFlag.Value = ""; var type = GetCardType(reader, card); db.Add(new Card { Id = card, User = user, Type = type }); await db.SaveChangesAsync(); return Ok(new UserResponse(user)); } /// /// Creates a transaction that changes the balance of the the user the card is linked to by the specified amount. /// /// The ID of the card /// Random string (idempotency key) which is consistent across request retries /// Positive or negative number of cents representing the relative change in balance /// Type of reader that scanned the card /// Returns 200 if the transaction succeeded /// Returns 404 if the card isn't linked to any account /// Returns 412 if the transaction failed because the balance would be out of range after the transaction [HttpPut] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] [ProducesResponseType(StatusCodes.Status412PreconditionFailed, Type = typeof(ErrorResponse))] [SwaggerResponseExample(200, typeof(UserUpdatedExample))] [SwaggerResponseExample(404, typeof(ErrorUnknownCardExample))] [SwaggerResponseExample(412, typeof(ErrorBalanceOutOfRangeExample))] [Route("/api/card/{card}/transaction/{ik}")] public async Task Transaction(string card, string ik, [FromQuery] int amount, [FromQuery] string? reader) { var db = new DatabaseContext(); if (db.Cards.Any(p => p.Id == card)) { var user = db.Cards.Include(p => p.User).First(p => p.Id == card).User; if (ik == "" || ik != user.LastIdempotencyKey) { user.LastIdempotencyKey = ik; switch (user.Balance + amount) { case < -9999: return StatusCode(412, new ErrorResponse("Balance too low!")); case > 99999: return StatusCode(412, new ErrorResponse("Balance too high!")); } user.Balance += amount; } var dbCard = db.Cards.First(p => p.Id == card); var newType = GetCardType(reader, card); if (dbCard.Type != newType) dbCard.Type = newType; await db.SaveChangesAsync(); return Ok(new UserResponse(user)); } return NotFound(new ErrorResponse("Unknown card.")); } /// /// Returns the balance of the the user the card is linked to. /// /// The ID of the card /// Type of reader that scanned the card /// Returns 200 if the request succeeded /// Returns 404 if the card isn't linked to any account [HttpGet] [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] [SwaggerResponseExample(200, typeof(UserUpdatedExample))] [SwaggerResponseExample(404, typeof(ErrorUnknownCardExample))] [Route("/api/card/{card}/balance")] public async Task Balance(string card, [FromQuery] string? reader) { var db = new DatabaseContext(); if (db.Cards.Any(p => p.Id == card)) { var user = db.Cards.Include(p => p.User).First(p => p.Id == card).User; var dbCard = db.Cards.First(p => p.Id == card); var newType = GetCardType(reader, card); if (dbCard.Type != newType) { dbCard.Type = newType; await db.SaveChangesAsync(); } return Ok(new UserResponse(user)); } return NotFound(new ErrorResponse("Unknown card.")); } private class UserUpdatedExample : IExamplesProvider { public UserResponse GetExamples() => new(new User { Id = 123, Nickname = "testman", Balance = 2550, LastIdempotencyKey = "5a6c94aa" }); } private class ErrorUnknownCardExample : IExamplesProvider { public ErrorResponse GetExamples() => new("Unknown card"); } private class ErrorBalanceOutOfRangeExample : IExamplesProvider { public ErrorResponse GetExamples() => new("Balance out of range"); } private class ErrorNoActiveLinkProcessExample : IExamplesProvider { public ErrorResponse GetExamples() => new("No active link process"); } private Card.CardType GetCardType(string? reader, string cardNumber) { //TODO match more specific type based on card number format return reader switch { "rdm6300" => Card.CardType.Rfid125KhzGeneric, "pn532-iso14443a" => Card.CardType.NfcGeneric, "pn532-felica" => Card.CardType.FeliCaGeneric, _ => Card.CardType.Unknown }; } }