166 lines
6.7 KiB
C#
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
|
|
};
|
|
}
|
|
}
|