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 System.Linq;
using IO.Swagger.Services; using IO.Swagger.Services;
using System.Security.Claims; using System.Security.Claims;
using AutoMapper;
using Newtonsoft.Json.Linq;
namespace IO.Swagger.Controllers namespace IO.Swagger.Controllers
{ {
@ -34,17 +36,20 @@ namespace IO.Swagger.Controllers
{ {
private readonly IUserRepository repository; private readonly IUserRepository repository;
private readonly JwtService jwt; private readonly JwtService jwt;
private readonly IMapper mapper;
/// <summary> /// <summary>
/// The controller for the authotization endpoints /// The controller for the authotization endpoints
/// </summary> /// </summary>
/// <param name="repository"></param> /// <param name="repository"></param>
/// <param name="jwt"></param> /// <param name="jwt"></param>
/// <param name="mapper"></param>
/// <exception cref="ArgumentNullException"></exception> /// <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.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.jwt = jwt ?? throw new ArgumentNullException(nameof(jwt)); this.jwt = jwt ?? throw new ArgumentNullException(nameof(jwt));
this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
} }
/// <summary> /// <summary>
@ -57,13 +62,15 @@ namespace IO.Swagger.Controllers
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)] [Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("GetUserDetails")] [SwaggerOperation("GetUserDetails")]
[ProducesResponseType(typeof(UserDto), 200)]
[ProducesResponseType(401)]
public virtual async Task<IActionResult> GetUserDetails() public virtual async Task<IActionResult> GetUserDetails()
{ {
var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
if (!int.TryParse(userIdString, out int userId)) if (!int.TryParse(userIdString, out int userId))
return Unauthorized(); return Unauthorized();
var user = await repository.RetrieveUser(userId); var user = await repository.RetrieveUser(userId);
return user == null ? NoContent() : Ok(user); return Ok(mapper.Map<UserDto>(user));
} }
/// <summary> /// <summary>
@ -71,17 +78,21 @@ namespace IO.Swagger.Controllers
/// </summary> /// </summary>
/// <param name="body"></param> /// <param name="body"></param>
/// <response code="200">Logged in successfully</response> /// <response code="200">Logged in successfully</response>
/// <response code="400">Bad Request</response>
/// <response code="401">Unauthorized</response> /// <response code="401">Unauthorized</response>
[HttpPost] [HttpPost]
[Route("/v1/api/auth/login")] [Route("/v1/api/auth/login")]
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("LoginUser")] [SwaggerOperation("LoginUser")]
[ProducesResponseType(typeof(TokenDto), 200)]
[ProducesResponseType(typeof(IEnumerable<string>), 400)]
[ProducesResponseType(401)]
public virtual async Task<IActionResult> LoginUser([FromBody]AuthLoginBody body) public virtual async Task<IActionResult> LoginUser([FromBody]AuthLoginBody body)
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
return BadRequest(ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); return BadRequest(ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
var user = await repository.LoginUser(body); 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> /// <summary>
@ -96,13 +107,16 @@ namespace IO.Swagger.Controllers
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("RegisterUser")] [SwaggerOperation("RegisterUser")]
[ProducesResponseType(typeof(TokenDto), 200)]
[ProducesResponseType(typeof(IEnumerable<string>), 400)]
[ProducesResponseType(409)]
public async Task<IActionResult> RegisterUser([FromBody]AuthRegisterBody body) public async Task<IActionResult> RegisterUser([FromBody]AuthRegisterBody body)
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
return BadRequest(ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage))); return BadRequest(ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
var user = await repository.RegisterUser(body); 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)] [Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("CreateCurrency")] [SwaggerOperation("CreateCurrency")]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(IEnumerable<string>), 400)]
[ProducesResponseType(401)]
[ProducesResponseType(422)]
public virtual async Task<IActionResult> CreateCurrency([FromBody]CurrencyCreateBody body) public virtual async Task<IActionResult> CreateCurrency([FromBody]CurrencyCreateBody body)
{ {
var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
@ -129,6 +133,10 @@ namespace IO.Swagger.Controllers
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)] [Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("MintCurrency")] [SwaggerOperation("MintCurrency")]
[ProducesResponseType(200)]
[ProducesResponseType(typeof(IEnumerable<string>), 400)]
[ProducesResponseType(401)]
[ProducesResponseType(409)]
public virtual async Task<IActionResult> MintCurrency([FromBody]CurrencyMintBody body) public virtual async Task<IActionResult> MintCurrency([FromBody]CurrencyMintBody body)
{ {
var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; 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 IO.Swagger.Security;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using IO.Swagger.Models.dto; 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 namespace IO.Swagger.Controllers
{ {
@ -26,26 +31,67 @@ namespace IO.Swagger.Controllers
/// </summary> /// </summary>
[ApiController] [ApiController]
public class WalletApiController : ControllerBase 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> /// <summary>
/// Get user&#x27;s wallet /// Get user&#x27;s wallet balances
/// </summary> /// </summary>
/// <response code="200">Successful response</response> /// <response code="200">Successful response</response>
/// <response code="401">Unauthorized</response> /// <response code="401">Unauthorized</response>
[HttpGet] [HttpGet]
[Route("/v1/api/wallet")] [Route("/v1/api/wallet/balances")]
[Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)] [Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)]
[ValidateModelState] [ValidateModelState]
[SwaggerOperation("GetUserWallet")] [SwaggerOperation("GetUserBalances")]
public virtual IActionResult GetUserWallet() [ProducesResponseType(typeof(WalletBalanceDto), 200)]
{ [ProducesResponseType(401)]
//TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ... public virtual async Task<IActionResult> GetUserBalances()
// 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(..), ... var transactions = await transactionRepository.GetTransactionsForUser(userId);
// return StatusCode(401); 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> /// <summary>

View File

@ -9,6 +9,7 @@
<PackageId>IO.Swagger</PackageId> <PackageId>IO.Swagger</PackageId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" 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;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text.Json.Serialization;
namespace IO.Swagger.Models.db namespace IO.Swagger.Models.db
{ {
@ -17,7 +18,6 @@ namespace IO.Swagger.Models.db
[ForeignKey("FK_Currency_UserId")] [ForeignKey("FK_Currency_UserId")]
public int UserId { get; set; } public int UserId { get; set; }
public User User { get; set; } public User User { get; set; }
public ICollection<Transaction> Transactions { get; set; } public ICollection<Transaction> Transactions { get; set; }
} }
} }

View File

@ -2,6 +2,7 @@
using Microsoft.VisualBasic; using Microsoft.VisualBasic;
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace IO.Swagger.Models.db namespace IO.Swagger.Models.db
{ {
@ -19,7 +20,7 @@ namespace IO.Swagger.Models.db
[Required] [Required]
[StringLength(32, MinimumLength = 2)] [StringLength(32, MinimumLength = 2)]
public string Memo { get; set; } public string Memo { get; set; }
public Currency Currency { get; set; } public Currency Currency { get; set; }
public int CurrencyId { get; set; } public int CurrencyId { get; set; }

View File

@ -1,12 +1,14 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace IO.Swagger.Models.db namespace IO.Swagger.Models.db
{ {
public class User public class User
{ {
public int Id { get; set; } public int Id { get; set; }
public string Email { get; set; } public string Email { get; set; }
[StringLength(32, MinimumLength = 3)] [StringLength(32, MinimumLength = 3)]
public string FirstName { get; set; } public string FirstName { get; set; }
@ -14,9 +16,7 @@ namespace IO.Swagger.Models.db
public string LastName { get; set; } public string LastName { get; set; }
public string PasswordHash { get; set; } public string PasswordHash { get; set; }
public string Salt { get; set; } public string Salt { get; set; }
public ICollection<Currency> Currencies { get; set; } public ICollection<Currency> Currencies { get; set; }
public ICollection<Transaction> TransactionsFrom { get; set; } public ICollection<Transaction> TransactionsFrom { get; set; }
public ICollection<Transaction> TransactionsTo { 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.Repositories;
using IO.Swagger.Services; using IO.Swagger.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using AutoMapper;
namespace IO.Swagger 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 => { services.AddCors(opt => {
opt.AddDefaultPolicy(po => opt.AddDefaultPolicy(po =>
{ {
@ -132,17 +141,22 @@ namespace IO.Swagger
}); });
}); });
//Datase connections
string connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING"); string connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING");
if (string.IsNullOrEmpty(connectionString)) if (string.IsNullOrEmpty(connectionString))
{ {
throw new Exception("Database connection string not found in environment variable."); throw new Exception("Database connection string not found in environment variable.");
} }
Console.WriteLine(connectionString);
// DI setup
services.AddScoped<IUserRepository, UserRepository>(); services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<ICurrencyRepository, CurrencyRepository>(); services.AddScoped<ICurrencyRepository, CurrencyRepository>();
services.AddScoped<ITransactionRepository, TransactionRepository>();
services.AddDbContext<BankDbContext>(x => x.UseSqlServer(connectionString: connectionString)); services.AddDbContext<BankDbContext>(x => x.UseSqlServer(connectionString: connectionString));
services.AddSingleton<JwtService>(); services.AddSingleton<JwtService>();
IMapper mapper = mapperConfig.CreateMapper();
services.AddSingleton(mapper);
} }
/// <summary> /// <summary>