Setup wallet and transaction retrieval, better swagger docs, and proper dtos

This commit is contained in:
2023-08-19 16:43:58 -04:00
parent 658bd7ca2a
commit dc9ab74598
16 changed files with 224 additions and 21 deletions

View File

@ -23,6 +23,8 @@ using System.Threading.Tasks;
using System.Linq;
using IO.Swagger.Services;
using System.Security.Claims;
using AutoMapper;
using Newtonsoft.Json.Linq;
namespace IO.Swagger.Controllers
{
@ -34,17 +36,20 @@ namespace IO.Swagger.Controllers
{
private readonly IUserRepository repository;
private readonly JwtService jwt;
private readonly IMapper mapper;
/// <summary>
/// The controller for the authotization endpoints
/// </summary>
/// <param name="repository"></param>
/// <param name="jwt"></param>
/// <param name="mapper"></param>
/// <exception cref="ArgumentNullException"></exception>
public AuthApiController(IUserRepository repository, JwtService jwt)
public AuthApiController(IUserRepository repository, JwtService jwt, IMapper mapper)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.jwt = jwt ?? throw new ArgumentNullException(nameof(jwt));
this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
/// <summary>
@ -57,13 +62,15 @@ namespace IO.Swagger.Controllers
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState]
[SwaggerOperation("GetUserDetails")]
[ProducesResponseType(typeof(UserDto), 200)]
[ProducesResponseType(401)]
public virtual async Task<IActionResult> GetUserDetails()
{
var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
if (!int.TryParse(userIdString, out int userId))
return Unauthorized();
var user = await repository.RetrieveUser(userId);
return user == null ? NoContent() : Ok(user);
return Ok(mapper.Map<UserDto>(user));
}
/// <summary>
@ -71,17 +78,21 @@ namespace IO.Swagger.Controllers
/// </summary>
/// <param name="body"></param>
/// <response code="200">Logged in successfully</response>
/// <response code="400">Bad Request</response>
/// <response code="401">Unauthorized</response>
[HttpPost]
[Route("/v1/api/auth/login")]
[ValidateModelState]
[SwaggerOperation("LoginUser")]
[ProducesResponseType(typeof(TokenDto), 200)]
[ProducesResponseType(typeof(IEnumerable<string>), 400)]
[ProducesResponseType(401)]
public virtual async Task<IActionResult> LoginUser([FromBody]AuthLoginBody body)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
var user = await repository.LoginUser(body);
return user == null ? Unauthorized() : Ok(new { token = jwt.GenerateJwt(user.Id) });
return user == null ? Unauthorized() : Ok(new TokenDto{ Token = jwt.GenerateJwt(user.Id) });
}
/// <summary>
@ -96,13 +107,16 @@ namespace IO.Swagger.Controllers
[ValidateModelState]
[SwaggerOperation("RegisterUser")]
[ProducesResponseType(typeof(TokenDto), 200)]
[ProducesResponseType(typeof(IEnumerable<string>), 400)]
[ProducesResponseType(409)]
public async Task<IActionResult> RegisterUser([FromBody]AuthRegisterBody body)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
var user = await repository.RegisterUser(body);
return user == null ? StatusCode(409) : Ok(new { token = jwt.GenerateJwt(user.Id) });
return user == null ? StatusCode(409) : Ok(new TokenDto{ Token = jwt.GenerateJwt(user.Id) });
}
}
}

View File

@ -103,6 +103,10 @@ namespace IO.Swagger.Controllers
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState]
[SwaggerOperation("CreateCurrency")]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(IEnumerable<string>), 400)]
[ProducesResponseType(401)]
[ProducesResponseType(422)]
public virtual async Task<IActionResult> CreateCurrency([FromBody]CurrencyCreateBody body)
{
var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
@ -129,6 +133,10 @@ namespace IO.Swagger.Controllers
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState]
[SwaggerOperation("MintCurrency")]
[ProducesResponseType(200)]
[ProducesResponseType(typeof(IEnumerable<string>), 400)]
[ProducesResponseType(401)]
[ProducesResponseType(409)]
public virtual async Task<IActionResult> MintCurrency([FromBody]CurrencyMintBody body)
{
var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;

View File

@ -18,6 +18,11 @@ using IO.Swagger.Attributes;
using IO.Swagger.Security;
using Microsoft.AspNetCore.Authorization;
using IO.Swagger.Models.dto;
using System.Linq;
using System.Security.Claims;
using IO.Swagger.Repositories;
using System.Threading.Tasks;
using AutoMapper;
namespace IO.Swagger.Controllers
{
@ -27,25 +32,66 @@ namespace IO.Swagger.Controllers
[ApiController]
public class WalletApiController : ControllerBase
{
private readonly ITransactionRepository transactionRepository;
private readonly IMapper mapper;
public WalletApiController(ITransactionRepository transactionRepository, IMapper mapper)
{
this.transactionRepository = transactionRepository ?? throw new ArgumentNullException(nameof(transactionRepository));
this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
/// <summary>
/// Get user&#x27;s wallet
/// Get user&#x27;s wallet balances
/// </summary>
/// <response code="200">Successful response</response>
/// <response code="401">Unauthorized</response>
[HttpGet]
[Route("/v1/api/wallet")]
[Route("/v1/api/wallet/balances")]
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState]
[SwaggerOperation("GetUserWallet")]
public virtual IActionResult GetUserWallet()
[SwaggerOperation("GetUserBalances")]
[ProducesResponseType(typeof(WalletBalanceDto), 200)]
[ProducesResponseType(401)]
public virtual async Task<IActionResult> GetUserBalances()
{
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(200);
var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
if (!int.TryParse(userIdString, out int userId))
return Unauthorized();
//TODO: Uncomment the next line to return response 401 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
// return StatusCode(401);
var transactions = await transactionRepository.GetTransactionsForUser(userId);
var balances = transactions.GroupBy(t => t.Currency)
.Select(g => new WalletBalanceDto
{
Currency = mapper.Map<CurrencyDto>(g.Key),
Balance = g.Sum(t =>t.Amount)
});
return Ok(balances);
}
throw new NotImplementedException();
/// <summary>
/// Get user&#x27;s wallet transactions
/// </summary>
/// <response code="200">Successful response</response>
/// <response code="401">Unauthorized</response>
[HttpGet]
[Route("/v1/api/wallet/transactions")]
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState]
[SwaggerOperation("GetUserTransactions")]
[ProducesResponseType(typeof(IEnumerable<TransactionDto>), 200)]
[ProducesResponseType(401)]
public virtual async Task<IActionResult> GetUserTransactions()
{
var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
if (!int.TryParse(userIdString, out int userId))
return Unauthorized();
var transactions = await transactionRepository.GetTransactionsForUser(userId);
return Ok(transactions.Select(mapper.Map<TransactionDto>));
}
/// <summary>

View File

@ -9,6 +9,7 @@
<PackageId>IO.Swagger</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.10">

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace IO.Swagger.Models.db
{
@ -17,7 +18,6 @@ namespace IO.Swagger.Models.db
[ForeignKey("FK_Currency_UserId")]
public int UserId { get; set; }
public User User { get; set; }
public ICollection<Transaction> Transactions { get; set; }
}
}

View File

@ -2,6 +2,7 @@
using Microsoft.VisualBasic;
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace IO.Swagger.Models.db
{

View File

@ -1,12 +1,14 @@
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace IO.Swagger.Models.db
{
public class User
{
public int Id { get; set; }
public string Email { get; set; }
[StringLength(32, MinimumLength = 3)]
public string FirstName { get; set; }
@ -14,9 +16,7 @@ namespace IO.Swagger.Models.db
public string LastName { get; set; }
public string PasswordHash { get; set; }
public string Salt { get; set; }
public ICollection<Currency> Currencies { get; set; }
public ICollection<Transaction> TransactionsFrom { get; set; }
public ICollection<Transaction> TransactionsTo { get; set; }
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace IO.Swagger.Models.dto
{
public class CurrencyDto
{
public int CurrencyId { get; set; }
[StringLength(32, MinimumLength = 1)]
public string Name { get; set; }
[StringLength(4, MinimumLength = 1)]
public string Symbol { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace IO.Swagger.Models.dto
{
public class TokenDto
{
public string Token { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.SqlServer.Server;
using Microsoft.VisualBasic;
using System;
namespace IO.Swagger.Models.dto
{
public class TransactionDto
{
public UserDto FromUser { get; set; }
public UserDto ToUser { get; set; }
public float Amount { get; set; }
public CurrencyDto Currency { get; set; }
public string Memo { get; set; }
public DateTime TransactionTime { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using IO.Swagger.Models.db;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace IO.Swagger.Models.dto
{
public class UserDto
{
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace IO.Swagger.Models.dto
{
public class WalletBalanceDto
{
public CurrencyDto Currency { get; set; }
public float Balance { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using IO.Swagger.Models.db;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace IO.Swagger.Repositories
{
public interface ITransactionRepository
{
Task<List<Transaction>> GetTransactionsForUser(int userId);
}
}

View File

@ -0,0 +1,35 @@
using IO.Swagger.Models.db;
using IO.Swagger.Services;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace IO.Swagger.Repositories
{
public class TransactionRepository : ITransactionRepository
{
BankDbContext context;
public TransactionRepository(BankDbContext context)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<List<Transaction>> GetTransactionsForUser(int userId)
{
var transactions = await context.Transactions.Where(t => t.ToUserId == userId || t.FromUserId == userId)
.Include(t => t.Currency)
.Include(t => t.FromUser)
.Include(t => t.ToUser)
.OrderByDescending(t => t.TransactionTime)
.ToListAsync();
foreach (var t in transactions)
if (t.ToUserId != t.FromUserId && t.FromUserId == userId)
t.Amount *= -1;
return transactions;
}
}
}

View File

@ -0,0 +1,16 @@
using AutoMapper;
using IO.Swagger.Models.db;
using IO.Swagger.Models.dto;
namespace IO.Swagger.Services
{
public class MapperProfile : Profile
{
public MapperProfile()
{
CreateMap<User, UserDto>();
CreateMap<Currency, CurrencyDto>();
CreateMap<Transaction, TransactionDto>();
}
}
}

View File

@ -26,6 +26,7 @@ using IO.Swagger.Security;
using IO.Swagger.Repositories;
using IO.Swagger.Services;
using Microsoft.EntityFrameworkCore;
using AutoMapper;
namespace IO.Swagger
{
@ -122,6 +123,14 @@ namespace IO.Swagger
});
});
// Auto Mapper Configurations
var mapperConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new MapperProfile());
});
// CORS sucks
services.AddCors(opt => {
opt.AddDefaultPolicy(po =>
{
@ -132,17 +141,22 @@ namespace IO.Swagger
});
});
//Datase connections
string connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING");
if (string.IsNullOrEmpty(connectionString))
{
throw new Exception("Database connection string not found in environment variable.");
}
Console.WriteLine(connectionString);
// DI setup
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<ICurrencyRepository, CurrencyRepository>();
services.AddScoped<ITransactionRepository, TransactionRepository>();
services.AddDbContext<BankDbContext>(x => x.UseSqlServer(connectionString: connectionString));
services.AddSingleton<JwtService>();
IMapper mapper = mapperConfig.CreateMapper();
services.AddSingleton(mapper);
}
/// <summary>