diff --git a/src/Services/Product/Product.API/Controllers/ProductController.cs b/src/Services/Product/Product.API/Controllers/ProductController.cs index 710c58f..e9d1c01 100644 --- a/src/Services/Product/Product.API/Controllers/ProductController.cs +++ b/src/Services/Product/Product.API/Controllers/ProductController.cs @@ -1,29 +1,218 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Product.Domain.DTOs; +using Product.Infrastructure.Data; namespace Product.API.Controllers; [ApiController] [Route("api/[controller]")] +[Authorize] public class ProductController : ControllerBase { - private readonly ILogger _logger; + private readonly ProductDbContext _db; - public ProductController(ILogger logger) + public ProductController(ProductDbContext db) { - _logger = logger; + _db = db; } [HttpGet] - public IActionResult GetAll() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetAll() { - // TODO: Implement — migrate logic from monolith's ProductController - return Ok(new { service = "Product", status = "scaffold" }); + var products = await _db.Products + .Include(p => p.ProductCategory) + .Select(p => new ProductDto + { + Id = p.Id, + Name = p.Name, + Description = p.Description, + Icon = p.Icon, + BuyingPrice = p.BuyingPrice, + SellingPrice = p.SellingPrice, + UnitsInStock = p.UnitsInStock, + IsActive = p.IsActive, + IsDiscontinued = p.IsDiscontinued, + ProductCategoryName = p.ProductCategory.Name + }) + .ToListAsync(); + + return Ok(products); } [HttpGet("{id}")] - public IActionResult GetById(int id) + [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(int id) + { + var product = await _db.Products + .Include(p => p.ProductCategory) + .Where(p => p.Id == id) + .Select(p => new ProductDto + { + Id = p.Id, + Name = p.Name, + Description = p.Description, + Icon = p.Icon, + BuyingPrice = p.BuyingPrice, + SellingPrice = p.SellingPrice, + UnitsInStock = p.UnitsInStock, + IsActive = p.IsActive, + IsDiscontinued = p.IsDiscontinued, + ProductCategoryName = p.ProductCategory.Name + }) + .FirstOrDefaultAsync(); + + if (product is null) return NotFound(); + return Ok(product); + } + + [HttpPost] + [ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)] + public async Task Create([FromBody] CreateProductRequest request) + { + var category = await _db.ProductCategories.FindAsync(request.ProductCategoryId); + if (category is null) return BadRequest("Invalid ProductCategoryId"); + + var entity = new Product.Domain.Entities.Product + { + Name = request.Name, + Description = request.Description, + Icon = request.Icon, + BuyingPrice = request.BuyingPrice, + SellingPrice = request.SellingPrice, + UnitsInStock = request.UnitsInStock, + IsActive = request.IsActive, + IsDiscontinued = request.IsDiscontinued, + ParentId = request.ParentId, + ProductCategoryId = request.ProductCategoryId, + ProductCategory = category, + CreatedDate = DateTime.UtcNow, + UpdatedDate = DateTime.UtcNow + }; + + _db.Products.Add(entity); + await _db.SaveChangesAsync(); + + var dto = new ProductDto + { + Id = entity.Id, + Name = entity.Name, + Description = entity.Description, + Icon = entity.Icon, + BuyingPrice = entity.BuyingPrice, + SellingPrice = entity.SellingPrice, + UnitsInStock = entity.UnitsInStock, + IsActive = entity.IsActive, + IsDiscontinued = entity.IsDiscontinued, + ProductCategoryName = category.Name + }; + + return CreatedAtAction(nameof(GetById), new { id = entity.Id }, dto); + } + + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(int id, [FromBody] CreateProductRequest request) { - // TODO: Implement — migrate logic from monolith - return Ok(new { service = "Product", id }); + var entity = await _db.Products.FindAsync(id); + if (entity is null) return NotFound(); + + var category = await _db.ProductCategories.FindAsync(request.ProductCategoryId); + if (category is null) return BadRequest("Invalid ProductCategoryId"); + + entity.Name = request.Name; + entity.Description = request.Description; + entity.Icon = request.Icon; + entity.BuyingPrice = request.BuyingPrice; + entity.SellingPrice = request.SellingPrice; + entity.UnitsInStock = request.UnitsInStock; + entity.IsActive = request.IsActive; + entity.IsDiscontinued = request.IsDiscontinued; + entity.ParentId = request.ParentId; + entity.ProductCategoryId = request.ProductCategoryId; + entity.UpdatedDate = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + return NoContent(); + } + + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(int id) + { + var entity = await _db.Products.FindAsync(id); + if (entity is null) return NotFound(); + + _db.Products.Remove(entity); + await _db.SaveChangesAsync(); + return NoContent(); + } + + [HttpGet("categories")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetCategories() + { + var categories = await _db.ProductCategories + .Select(c => new CategoryDto + { + Id = c.Id, + Name = c.Name, + Description = c.Description, + Icon = c.Icon + }) + .ToListAsync(); + + return Ok(categories); + } + + [HttpPost("categories")] + [ProducesResponseType(typeof(CategoryDto), StatusCodes.Status201Created)] + public async Task CreateCategory([FromBody] CreateCategoryRequest request) + { + var entity = new Product.Domain.Entities.ProductCategory + { + Name = request.Name, + Description = request.Description, + Icon = request.Icon, + CreatedDate = DateTime.UtcNow, + UpdatedDate = DateTime.UtcNow + }; + + _db.ProductCategories.Add(entity); + await _db.SaveChangesAsync(); + + var dto = new CategoryDto + { + Id = entity.Id, + Name = entity.Name, + Description = entity.Description, + Icon = entity.Icon + }; + + return CreatedAtAction(nameof(GetCategories), dto); } } + +public record CreateProductRequest( + string Name, + string? Description, + string? Icon, + decimal BuyingPrice, + decimal SellingPrice, + int UnitsInStock, + bool IsActive, + bool IsDiscontinued, + int? ParentId, + int ProductCategoryId +); + +public record CreateCategoryRequest( + string Name, + string? Description, + string? Icon +); diff --git a/src/Services/Product/Product.API/Product.API.csproj b/src/Services/Product/Product.API/Product.API.csproj index 68be876..cd87d99 100644 --- a/src/Services/Product/Product.API/Product.API.csproj +++ b/src/Services/Product/Product.API/Product.API.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Services/Product/Product.API/Program.cs b/src/Services/Product/Product.API/Program.cs index 0ae7c5f..bfe94fa 100644 --- a/src/Services/Product/Product.API/Program.cs +++ b/src/Services/Product/Product.API/Program.cs @@ -1,3 +1,6 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; using Product.Infrastructure.Data; using Microsoft.EntityFrameworkCore; @@ -11,6 +14,30 @@ builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); +// JWT Authentication +var jwtSection = builder.Configuration.GetSection("Jwt"); +var key = Encoding.UTF8.GetBytes(jwtSection["Secret"]!); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSection["Issuer"], + ValidAudience = jwtSection["Audience"], + IssuerSigningKey = new SymmetricSecurityKey(key) + }; +}); +builder.Services.AddAuthorization(); + var app = builder.Build(); using (var scope = app.Services.CreateScope()) @@ -25,6 +52,9 @@ app.UseSwaggerUI(); } +app.UseAuthentication(); +app.UseAuthorization(); + app.MapControllers(); app.MapHealthChecks("/healthz"); diff --git a/src/Services/Product/Product.API/appsettings.json b/src/Services/Product/Product.API/appsettings.json index b998b08..2ffbfd7 100644 --- a/src/Services/Product/Product.API/appsettings.json +++ b/src/Services/Product/Product.API/appsettings.json @@ -7,5 +7,10 @@ }, "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=productdb;Username=postgres;Password=postgres" + }, + "Jwt": { + "Secret": "QuickApp_Microservices_SuperSecret_Key_For_Dev_Only_Min_32_Chars!", + "Issuer": "quickapp-identity", + "Audience": "quickapp-api" } }