AfRApay/AfRApay.Web/Controllers/CardController.cs

166 lines
6.7 KiB
C#

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 {
/// <summary>
/// Links the given card to the user currently in link mode.
/// </summary>
/// <param name="card">The ID of the card</param>
/// <param name="reader">Type of reader that scanned the card</param>
/// <response code="200">Returns 200 if the link succeeded</response>
/// <response code="304">Returns 304 if the card was already linked</response>
/// <response code="404">Returns 404 if no link process is active</response>
[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<IActionResult> 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));
}
/// <summary>
/// Creates a transaction that changes the balance of the the user the card is linked to by the specified amount.
/// </summary>
/// <param name="card">The ID of the card</param>
/// <param name="ik">Random string (idempotency key) which is consistent across request retries</param>
/// <param name="amount">Positive or negative number of cents representing the relative change in balance</param>
/// <param name="reader">Type of reader that scanned the card</param>
/// <response code="200">Returns 200 if the transaction succeeded</response>
/// <response code="404">Returns 404 if the card isn't linked to any account</response>
/// <response code="412">Returns 412 if the transaction failed because the balance would be out of range after the transaction</response>
[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<IActionResult> 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."));
}
/// <summary>
/// Returns the balance of the the user the card is linked to.
/// </summary>
/// <param name="card">The ID of the card</param>
/// <param name="reader">Type of reader that scanned the card</param>
/// <response code="200">Returns 200 if the request succeeded</response>
/// <response code="404">Returns 404 if the card isn't linked to any account</response>
[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<IActionResult> 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<UserResponse> {
public UserResponse GetExamples() => new(new User { Id = 123, Nickname = "testman", Balance = 2550, LastIdempotencyKey = "5a6c94aa" });
}
private class ErrorUnknownCardExample : IExamplesProvider<ErrorResponse> {
public ErrorResponse GetExamples() => new("Unknown card");
}
private class ErrorBalanceOutOfRangeExample : IExamplesProvider<ErrorResponse> {
public ErrorResponse GetExamples() => new("Balance out of range");
}
private class ErrorNoActiveLinkProcessExample : IExamplesProvider<ErrorResponse> {
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
};
}
}