diff --git a/src/IO.Swagger/Controllers/AuthApi.cs b/src/IO.Swagger/Controllers/AuthApi.cs index dee5b2b..b2af6b9 100644 --- a/src/IO.Swagger/Controllers/AuthApi.cs +++ b/src/IO.Swagger/Controllers/AuthApi.cs @@ -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; /// /// The controller for the authotization endpoints /// /// /// + /// /// - 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)); } /// @@ -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 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(user)); } /// @@ -71,17 +78,21 @@ namespace IO.Swagger.Controllers /// /// /// Logged in successfully + /// Bad Request /// Unauthorized [HttpPost] [Route("/v1/api/auth/login")] [ValidateModelState] [SwaggerOperation("LoginUser")] + [ProducesResponseType(typeof(TokenDto), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(401)] public virtual async Task 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) }); } /// @@ -96,13 +107,16 @@ namespace IO.Swagger.Controllers [ValidateModelState] [SwaggerOperation("RegisterUser")] + [ProducesResponseType(typeof(TokenDto), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(409)] public async Task 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) }); } } } diff --git a/src/IO.Swagger/Controllers/CurrencyApi.cs b/src/IO.Swagger/Controllers/CurrencyApi.cs index b0be919..9a74118 100644 --- a/src/IO.Swagger/Controllers/CurrencyApi.cs +++ b/src/IO.Swagger/Controllers/CurrencyApi.cs @@ -103,6 +103,10 @@ namespace IO.Swagger.Controllers [Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)] [ValidateModelState] [SwaggerOperation("CreateCurrency")] + [ProducesResponseType(201)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(401)] + [ProducesResponseType(422)] public virtual async Task 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), 400)] + [ProducesResponseType(401)] + [ProducesResponseType(409)] public virtual async Task MintCurrency([FromBody]CurrencyMintBody body) { var userIdString = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; diff --git a/src/IO.Swagger/Controllers/WalletApi.cs b/src/IO.Swagger/Controllers/WalletApi.cs index 0b22031..6363b1c 100644 --- a/src/IO.Swagger/Controllers/WalletApi.cs +++ b/src/IO.Swagger/Controllers/WalletApi.cs @@ -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 { @@ -26,26 +31,67 @@ 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)); + } + + + /// - /// Get user's wallet + /// Get user's wallet balances /// /// Successful response /// Unauthorized [HttpGet] - [Route("/v1/api/wallet")] + [Route("/v1/api/wallet/balances")] [Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)] [ValidateModelState] - [SwaggerOperation("GetUserWallet")] - public virtual IActionResult GetUserWallet() - { - //TODO: Uncomment the next line to return response 200 or use other options such as return this.NotFound(), return this.BadRequest(..), ... - // return StatusCode(200); + [SwaggerOperation("GetUserBalances")] + [ProducesResponseType(typeof(WalletBalanceDto), 200)] + [ProducesResponseType(401)] + public virtual async Task GetUserBalances() + { + 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(g.Key), + Balance = g.Sum(t =>t.Amount) + }); + return Ok(balances); + } - throw new NotImplementedException(); + /// + /// Get user's wallet transactions + /// + /// Successful response + /// Unauthorized + [HttpGet] + [Route("/v1/api/wallet/transactions")] + [Authorize(AuthenticationSchemes = BearerAuthenticationHandler.SchemeName)] + [ValidateModelState] + [SwaggerOperation("GetUserTransactions")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [ProducesResponseType(401)] + public virtual async Task 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)); } /// diff --git a/src/IO.Swagger/IO.Swagger.csproj b/src/IO.Swagger/IO.Swagger.csproj index 854fe09..e956357 100644 --- a/src/IO.Swagger/IO.Swagger.csproj +++ b/src/IO.Swagger/IO.Swagger.csproj @@ -9,6 +9,7 @@ IO.Swagger + diff --git a/src/IO.Swagger/Models/db/Currency.cs b/src/IO.Swagger/Models/db/Currency.cs index d820511..6cc3e39 100644 --- a/src/IO.Swagger/Models/db/Currency.cs +++ b/src/IO.Swagger/Models/db/Currency.cs @@ -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 Transactions { get; set; } } } diff --git a/src/IO.Swagger/Models/db/Transaction.cs b/src/IO.Swagger/Models/db/Transaction.cs index 79e3814..eef81d3 100644 --- a/src/IO.Swagger/Models/db/Transaction.cs +++ b/src/IO.Swagger/Models/db/Transaction.cs @@ -2,6 +2,7 @@ using Microsoft.VisualBasic; using System; using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace IO.Swagger.Models.db { @@ -19,7 +20,7 @@ namespace IO.Swagger.Models.db [Required] [StringLength(32, MinimumLength = 2)] public string Memo { get; set; } - + public Currency Currency { get; set; } public int CurrencyId { get; set; } diff --git a/src/IO.Swagger/Models/db/User.cs b/src/IO.Swagger/Models/db/User.cs index acc80c0..4c0b45b 100644 --- a/src/IO.Swagger/Models/db/User.cs +++ b/src/IO.Swagger/Models/db/User.cs @@ -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 Currencies { get; set; } - public ICollection TransactionsFrom { get; set; } public ICollection TransactionsTo { get; set; } } diff --git a/src/IO.Swagger/Models/dto/CurrencyDto.cs b/src/IO.Swagger/Models/dto/CurrencyDto.cs new file mode 100644 index 0000000..24038c0 --- /dev/null +++ b/src/IO.Swagger/Models/dto/CurrencyDto.cs @@ -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; } + } +} diff --git a/src/IO.Swagger/Models/dto/TokenDto.cs b/src/IO.Swagger/Models/dto/TokenDto.cs new file mode 100644 index 0000000..11e6f84 --- /dev/null +++ b/src/IO.Swagger/Models/dto/TokenDto.cs @@ -0,0 +1,7 @@ +namespace IO.Swagger.Models.dto +{ + public class TokenDto + { + public string Token { get; set; } + } +} diff --git a/src/IO.Swagger/Models/dto/TransactionDto.cs b/src/IO.Swagger/Models/dto/TransactionDto.cs new file mode 100644 index 0000000..510994e --- /dev/null +++ b/src/IO.Swagger/Models/dto/TransactionDto.cs @@ -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; } + } +} diff --git a/src/IO.Swagger/Models/dto/UserDto.cs b/src/IO.Swagger/Models/dto/UserDto.cs new file mode 100644 index 0000000..8fb115b --- /dev/null +++ b/src/IO.Swagger/Models/dto/UserDto.cs @@ -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; } + } +} diff --git a/src/IO.Swagger/Models/dto/WalletBalanceDto.cs b/src/IO.Swagger/Models/dto/WalletBalanceDto.cs new file mode 100644 index 0000000..d3f34cf --- /dev/null +++ b/src/IO.Swagger/Models/dto/WalletBalanceDto.cs @@ -0,0 +1,8 @@ +namespace IO.Swagger.Models.dto +{ + public class WalletBalanceDto + { + public CurrencyDto Currency { get; set; } + public float Balance { get; set; } + } +} diff --git a/src/IO.Swagger/Repositories/ITransactionRepository.cs b/src/IO.Swagger/Repositories/ITransactionRepository.cs new file mode 100644 index 0000000..d0ffe9a --- /dev/null +++ b/src/IO.Swagger/Repositories/ITransactionRepository.cs @@ -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> GetTransactionsForUser(int userId); + } +} diff --git a/src/IO.Swagger/Repositories/TransactionRepository.cs b/src/IO.Swagger/Repositories/TransactionRepository.cs new file mode 100644 index 0000000..c0263fc --- /dev/null +++ b/src/IO.Swagger/Repositories/TransactionRepository.cs @@ -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> 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; + } + } +} diff --git a/src/IO.Swagger/Services/MapperProfile.cs b/src/IO.Swagger/Services/MapperProfile.cs new file mode 100644 index 0000000..0f885b5 --- /dev/null +++ b/src/IO.Swagger/Services/MapperProfile.cs @@ -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(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/src/IO.Swagger/Startup.cs b/src/IO.Swagger/Startup.cs index 530da98..90197ba 100644 --- a/src/IO.Swagger/Startup.cs +++ b/src/IO.Swagger/Startup.cs @@ -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(); services.AddScoped(); + services.AddScoped(); services.AddDbContext(x => x.UseSqlServer(connectionString: connectionString)); services.AddSingleton(); + IMapper mapper = mapperConfig.CreateMapper(); + services.AddSingleton(mapper); } ///