API and database rework
This commit is contained in:
parent
9dfd45927c
commit
67e18ee192
|
@ -22,6 +22,7 @@ lib_deps =
|
|||
arduino-libraries/NTPClient@^3.2.1
|
||||
mcxiaoke/ESPDateTime@^1.0.4
|
||||
arduino12/rdm6300@^2.0.0
|
||||
bblanchon/ArduinoJson@^6.20.0
|
||||
check_tool = clangtidy
|
||||
check_flags =
|
||||
clangtidy: --checks=*,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-modernize-macro-to-enum
|
||||
|
|
|
@ -163,7 +163,7 @@ void loop() {
|
|||
updateOLED(u8g2, state, lastStatusText);
|
||||
tone(PIN_BUZZER, NOTE_A5, 25);
|
||||
tone(PIN_BUZZER, NOTE_NONE, 150);
|
||||
lastStatusText = cardTransaction(wifi, http, apiUrl, scannedCardId, "1.50");
|
||||
lastStatusText = cardTransaction(wifi, http, apiUrl, scannedCardId, "-150");
|
||||
if (lastStatusText.startsWith("S:")) {
|
||||
tone(PIN_BUZZER, NOTE_C7, 650);
|
||||
lastStatusText += "€";
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <HTTPClient.h>
|
||||
|
||||
|
@ -15,10 +16,23 @@ String uint32AsHexString(uint32_t input) {
|
|||
return byteArrayAsHexString(reinterpret_cast<byte *>(&input), sizeof input);
|
||||
}
|
||||
|
||||
String CentsToEuros(long cents){
|
||||
char euros[16];
|
||||
sprintf(euros, "%s%ld,%02ld", cents < 0 ? "-" : "", abs(cents / 100), abs(cents % 100));
|
||||
return euros;
|
||||
}
|
||||
|
||||
unsigned long cooldownSecondsRemaining(unsigned long timeout, unsigned long timer) {
|
||||
return (timeout - (millis() - timer)) / 1000 + 1;
|
||||
}
|
||||
|
||||
String getIdempotencyKey(){
|
||||
const int len = 16;
|
||||
byte buf[len];
|
||||
esp_fill_random(buf, len);
|
||||
return byteArrayAsHexString(buf, len);
|
||||
}
|
||||
|
||||
String splitString(String data, char separator, int index) {
|
||||
int found = 0;
|
||||
int strIndex[] = {0, -1};
|
||||
|
@ -36,13 +50,36 @@ String splitString(String data, char separator, int index) {
|
|||
|
||||
|
||||
String cardLink(WiFiClient *wifi, HTTPClient *http, String apiUrl, String cardId) {
|
||||
String finalRequestUrl = apiUrl + "/api/card/link?card=" + cardId;
|
||||
String finalRequestUrl = apiUrl + "/api/card/" + cardId + "/link";
|
||||
http->begin(*wifi, finalRequestUrl.c_str());
|
||||
int httpResponseCode = http->GET();
|
||||
if (httpResponseCode == 200) {
|
||||
http->addHeader("Content-Type", "application/json");
|
||||
int httpResponseCode = http->PUT("");
|
||||
if (httpResponseCode == 304)
|
||||
return "E:Already registered.";
|
||||
if (httpResponseCode == 200 || httpResponseCode == 404) {
|
||||
String payload = http->getString();
|
||||
http->end();
|
||||
return payload;
|
||||
Serial.println(payload);
|
||||
StaticJsonDocument<256> json;
|
||||
DeserializationError error = deserializeJson(json, payload.c_str());
|
||||
|
||||
if (error) {
|
||||
Serial.println(error.c_str());
|
||||
return String("E:JsonError:") + httpResponseCode;
|
||||
}
|
||||
|
||||
const char* status = json["status"];
|
||||
|
||||
if (strcmp(status, "success") != 0) {
|
||||
const char* message = json["message"];
|
||||
return String("E:") + message;
|
||||
}
|
||||
|
||||
JsonObject data = json["data"];
|
||||
const char* nickname = data["nickname"];
|
||||
long balance = data["balance"];
|
||||
|
||||
return String("S:") + nickname + ":" + CentsToEuros(balance);
|
||||
}
|
||||
http->end();
|
||||
if (httpResponseCode > 0) {
|
||||
|
@ -52,13 +89,34 @@ String cardLink(WiFiClient *wifi, HTTPClient *http, String apiUrl, String cardId
|
|||
}
|
||||
|
||||
String cardBalance(WiFiClient *wifi, HTTPClient *http, String apiUrl, String cardId) {
|
||||
String finalRequestUrl = apiUrl + "/api/card/balance?card=" + cardId;
|
||||
String finalRequestUrl = apiUrl + "/api/card/" + cardId + "/balance";
|
||||
http->begin(*wifi, finalRequestUrl.c_str());
|
||||
http->addHeader("Content-Type", "application/json");
|
||||
int httpResponseCode = http->GET();
|
||||
if (httpResponseCode == 200) {
|
||||
if (httpResponseCode == 200 || httpResponseCode == 404) {
|
||||
String payload = http->getString();
|
||||
http->end();
|
||||
return payload;
|
||||
Serial.println(payload);
|
||||
StaticJsonDocument<256> json;
|
||||
DeserializationError error = deserializeJson(json, payload.c_str());
|
||||
|
||||
if (error) {
|
||||
Serial.println(error.c_str());
|
||||
return "E:JsonError";
|
||||
}
|
||||
|
||||
const char* status = json["status"];
|
||||
|
||||
if (strcmp(status, "success") != 0) {
|
||||
const char* message = json["message"];
|
||||
return String("E:") + message;
|
||||
}
|
||||
|
||||
JsonObject data = json["data"];
|
||||
const char* nickname = data["nickname"];
|
||||
long balance = data["balance"];
|
||||
|
||||
return String("S:") + nickname + ":" + CentsToEuros(balance);
|
||||
}
|
||||
http->end();
|
||||
if (httpResponseCode > 0) {
|
||||
|
@ -68,13 +126,34 @@ String cardBalance(WiFiClient *wifi, HTTPClient *http, String apiUrl, String car
|
|||
}
|
||||
|
||||
String cardTransaction(WiFiClient *wifi, HTTPClient *http, String apiUrl, String cardId, String amount) {
|
||||
String finalRequestUrl = apiUrl + "/api/card/transaction?card=" + cardId + "&amount=" + amount;
|
||||
String idempotencyKey = getIdempotencyKey();
|
||||
String finalRequestUrl = apiUrl + "/api/card/" + cardId + "/transaction/" + idempotencyKey + "?amount=" + amount;
|
||||
http->begin(*wifi, finalRequestUrl.c_str());
|
||||
int httpResponseCode = http->GET();
|
||||
if (httpResponseCode == 200) {
|
||||
http->addHeader("Content-Type", "application/json");
|
||||
int httpResponseCode = http->PUT("");
|
||||
if (httpResponseCode == 200 || httpResponseCode == 404 || httpResponseCode == 412) {
|
||||
String payload = http->getString();
|
||||
http->end();
|
||||
return payload;
|
||||
Serial.println(payload);
|
||||
StaticJsonDocument<256> json;
|
||||
DeserializationError error = deserializeJson(json, payload.c_str());
|
||||
|
||||
if (error) {
|
||||
return "E:JsonError";
|
||||
}
|
||||
|
||||
const char* status = json["status"];
|
||||
|
||||
if (strcmp(status, "success") != 0) {
|
||||
const char* message = json["message"];
|
||||
return String("E:") + message;
|
||||
}
|
||||
|
||||
JsonObject data = json["data"];
|
||||
const char* nickname = data["nickname"];
|
||||
long balance = data["balance"];
|
||||
|
||||
return String("S:") + nickname + ":" + CentsToEuros(balance);
|
||||
}
|
||||
http->end();
|
||||
if (httpResponseCode > 0) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Reflection;
|
||||
using AfRApay.Web.Backend;
|
||||
using LinqToDB.Data;
|
||||
using Swashbuckle.AspNetCore.Filters;
|
||||
|
||||
DataConnection.DefaultSettings = new Database.Settings();
|
||||
Migrations.RunMigrations();
|
||||
|
@ -11,7 +12,9 @@ builder.Services.AddRazorPages();
|
|||
builder.Services.AddSwaggerGen(options => {
|
||||
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
|
||||
options.ExampleFilters();
|
||||
});
|
||||
builder.Services.AddSwaggerExamplesFromAssemblies(Assembly.GetEntryAssembly());
|
||||
|
||||
#if (DEBUG)
|
||||
builder.Services.AddControllers().AddRazorRuntimeCompilation();
|
||||
|
|
|
@ -8,6 +8,6 @@ namespace AfRApay.Web.Backend.Tables;
|
|||
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 = "Balance")] public int Balance { get; set; }
|
||||
[Column(Name = "LastIdempotencyKey")] public string? LastIdempotencyKey { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
using AfRApay.Web.Backend;
|
||||
using AfRApay.Web.Backend.Tables;
|
||||
using LinqToDB;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AfRApay.Web.Controllers;
|
||||
|
||||
[ApiController, Route("/api/card/balance")]
|
||||
public class CardBalance : Controller {
|
||||
[HttpGet]
|
||||
public string Get([FromQuery] string card) {
|
||||
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);
|
||||
return $"S:{user.Nickname}:{user.Balance:N2}";
|
||||
}
|
||||
|
||||
return "E:Unknown card.";
|
||||
}
|
||||
}
|
137
AfRApay.Web/Controllers/CardController.cs
Normal file
137
AfRApay.Web/Controllers/CardController.cs
Normal file
|
@ -0,0 +1,137 @@
|
|||
using AfRApay.Web.Backend;
|
||||
using AfRApay.Web.Backend.Tables;
|
||||
using AfRApay.Web.Controllers.Schema;
|
||||
using LinqToDB;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
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>
|
||||
/// <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) {
|
||||
var db = new Database.DbConn();
|
||||
if (db.Cards.Any(p => p.CardId == 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 = "";
|
||||
|
||||
await db.InsertAsync(new Card { CardId = card, UserId = user.Id });
|
||||
await db.UpdateAsync(linkFlag);
|
||||
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>
|
||||
/// <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) {
|
||||
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 (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;
|
||||
}
|
||||
|
||||
await db.UpdateAsync(user);
|
||||
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>
|
||||
/// <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 IActionResult Balance(string card) {
|
||||
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);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
using AfRApay.Web.Backend;
|
||||
using AfRApay.Web.Backend.Tables;
|
||||
using LinqToDB;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AfRApay.Web.Controllers;
|
||||
|
||||
[ApiController, Route("/api/card/link")]
|
||||
public class CardLink : Controller {
|
||||
[HttpGet]
|
||||
public string Get([FromQuery] string card) {
|
||||
var db = new Database.DbConn();
|
||||
if (db.Cards.Any(p => p.CardId == card)) {
|
||||
return "E:Already registered.";
|
||||
}
|
||||
|
||||
var linkFlag = db.Config.FirstOrDefault(p => p.Name == "link");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(linkFlag?.Value)) {
|
||||
return "E:No link flag set.";
|
||||
}
|
||||
|
||||
var lTimeFlag = db.Config.FirstOrDefault(p => p.Name == "lTime");
|
||||
if (string.IsNullOrWhiteSpace(lTimeFlag?.Value)) {
|
||||
return "E:No link flag set.";
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow - DateTime.Parse(lTimeFlag.Value) > TimeSpan.FromMinutes(5)) {
|
||||
return "E:Link expired, try again.";
|
||||
}
|
||||
|
||||
|
||||
var user = db.Users.First(p => p.Id == int.Parse(linkFlag.Value));
|
||||
linkFlag.Value = "";
|
||||
|
||||
db.Insert(new Card { CardId = card, UserId = user.Id });
|
||||
db.Update(linkFlag);
|
||||
return $"S:Reg. -> {user.Nickname}";
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
using AfRApay.Web.Backend;
|
||||
using LinqToDB;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AfRApay.Web.Controllers;
|
||||
|
||||
[ApiController, Route("/api/card/transaction")]
|
||||
public class CardTransaction : Controller {
|
||||
[HttpGet]
|
||||
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 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);
|
||||
}
|
||||
|
||||
return $"S:{user.Nickname}: {user.Balance:N2}";
|
||||
}
|
||||
|
||||
return "E:Unknown card.";
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
using AfRApay.Web.Backend;
|
||||
using AfRApay.Web.Controllers.Schema;
|
||||
using LinqToDB;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Filters;
|
||||
|
||||
namespace AfRApay.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
public class UserController : Controller {
|
||||
/// <summary>
|
||||
/// Renames the specified user.
|
||||
/// </summary>
|
||||
/// <param name="uid">The ID of the user to be renamed</param>
|
||||
/// <param name="newName">The new name of the user</param>
|
||||
/// <response code="200">Returns 200 if user was renamed successfully</response>
|
||||
/// <response code="400">Returns 400 if newName isn't unique</response>
|
||||
/// <response code="404">Returns 404 if no user matching the UID was found</response>
|
||||
[HttpPut]
|
||||
[Produces("application/json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
//[SwaggerResponseExample(400, typeof(new )]
|
||||
[Route("/api/user/{uid:int}/name")]
|
||||
public async Task<IActionResult> Rename(int uid, [FromQuery] string newName) {
|
||||
var db = new Database.DbConn();
|
||||
if (db.Users.Any(p => p.Id == uid)) {
|
||||
var user = db.Users.First(p => p.Id == uid);
|
||||
user.Nickname = newName;
|
||||
await db.UpdateAsync(user);
|
||||
return new OkObjectResult(new UserResponse(user));
|
||||
}
|
||||
|
||||
return NotFound(new ErrorResponse("Unknown user"));
|
||||
}
|
||||
}
|
|
@ -59,7 +59,7 @@
|
|||
<b>@user.Nickname</b>
|
||||
</td>
|
||||
<td>
|
||||
@($"{user.Balance:C}")
|
||||
@($"{user.Balance/100M:C}")
|
||||
</td>
|
||||
<td>
|
||||
<b>@db.Cards.Count(p => p.UserId == user.Id)</b> cards linked.
|
||||
|
@ -67,9 +67,9 @@
|
|||
<td> <!-- Displayed when in big layout -->
|
||||
<div class="d-none d-md-flex btn-group btn-group-lg" role="group">
|
||||
<!-- Make sure these buttons match the small/mobile layout ones below -->
|
||||
<a class="btn px-3 btn-danger" href="/UpdateBalance/@user.Id/-1.50">-1.50€</a>
|
||||
<a class="btn px-3 btn-success" href="/UpdateBalance/@user.Id/5">+5€</a>
|
||||
<a class="btn px-3 btn-success" href="/UpdateBalance/@user.Id/10">+10€</a>
|
||||
<a class="btn px-3 btn-danger" href="/Transaction/@user.Id/-150">-1.50€</a>
|
||||
<a class="btn px-3 btn-success" href="/Transaction/@user.Id/500">+5€</a>
|
||||
<a class="btn px-3 btn-success" href="/Transaction/@user.Id/1000">+10€</a>
|
||||
<a class="btn px-2 btn-primary" href="/EditUser/@user.Id">Edit</a>
|
||||
</div>
|
||||
<!-- Displayed when in compact/phone layout -->
|
||||
|
@ -80,9 +80,9 @@
|
|||
<div class="dropdown-menu p-1" aria-labelledby="dropdownMenuLink" style="min-width: max-content;"> <!-- Inline CSS, sets minimum width of drop down to maxium size of content -->
|
||||
<div class="d-grid gap-1">
|
||||
<!-- Make sure these buttons match the big layout ones above-->
|
||||
<a class="btn btn-lg btn-danger" href="/UpdateBalance/@user.Id/-1.50">-1.50€</a>
|
||||
<a class="btn btn-lg btn-success" href="/UpdateBalance/@user.Id/5">+5€</a>
|
||||
<a class="btn btn-lg btn-success" href="/UpdateBalance/@user.Id/10">+10€</a>
|
||||
<a class="btn btn-lg btn-danger" href="/UpdateBalance/@user.Id/-150">-1.50€</a>
|
||||
<a class="btn btn-lg btn-success" href="/UpdateBalance/@user.Id/500">+5€</a>
|
||||
<a class="btn btn-lg btn-success" href="/UpdateBalance/@user.Id/1000">+10€</a>
|
||||
<a class="btn btn-lg btn-primary" href="/EditUser/@user.Id">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@page "{id:int}/{amount:decimal}"
|
||||
@page "{id:int}/{amount:int}"
|
||||
@model UpdateBalanceModel
|
||||
@{
|
||||
Layout = null;
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
|
|||
namespace AfRApay.Web.Pages;
|
||||
|
||||
public class UpdateBalanceModel : PageModel {
|
||||
public void OnGet(int id, decimal amount) {
|
||||
public void OnGet(int id, int amount) {
|
||||
var db = new Database.DbConn();
|
||||
var user = db.Users.FirstOrDefault(p => p.Id == id);
|
||||
if (user == null) {
|
||||
|
@ -15,8 +15,8 @@ public class UpdateBalanceModel : PageModel {
|
|||
}
|
||||
|
||||
switch (user.Balance + amount) {
|
||||
case < -50: throw new ConstraintException("Balance too low!");
|
||||
case > 999: throw new ConstraintException("Balance too high!");
|
||||
case < -9999: throw new ConstraintException("Balance too low!");
|
||||
case > 99999: throw new ConstraintException("Balance too high!");
|
||||
}
|
||||
|
||||
user.Balance += amount;
|
Loading…
Reference in a new issue