From 101550af8f79200996ddf1838d1bb0ab276b03b4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 04:51:10 +0000 Subject: [PATCH] T3(customer): add full CRUD controller, service impl, DTO, and JWT auth --- .../Controllers/CustomerController.cs | 93 +++++++++++++++++-- .../Customer/Customer.API/Customer.API.csproj | 1 + .../Customer/Customer.API/DTOs/CustomerDto.cs | 12 +++ src/Services/Customer/Customer.API/Program.cs | 33 +++++++ .../Customer/Customer.API/appsettings.json | 6 ++ .../Customer.Infrastructure/Services/.gitkeep | 0 .../Services/CustomerService.cs | 51 ++++++++++ 7 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 src/Services/Customer/Customer.API/DTOs/CustomerDto.cs delete mode 100644 src/Services/Customer/Customer.Infrastructure/Services/.gitkeep create mode 100644 src/Services/Customer/Customer.Infrastructure/Services/CustomerService.cs diff --git a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs index 06cfead..67790a4 100644 --- a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs +++ b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs @@ -1,29 +1,108 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Customer.API.DTOs; +using Customer.Domain.Interfaces; +using Customer.Domain.Enums; +using Entities = Customer.Domain.Entities; namespace Customer.API.Controllers; [ApiController] [Route("api/[controller]")] +[Authorize] public class CustomerController : ControllerBase { + private readonly ICustomerService _customerService; private readonly ILogger _logger; - public CustomerController(ILogger logger) + public CustomerController(ICustomerService customerService, ILogger logger) { + _customerService = customerService; _logger = logger; } [HttpGet] - public IActionResult GetAll() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task GetAll() { - // TODO: Implement — migrate logic from monolith's CustomerController - return Ok(new { service = "Customer", status = "scaffold" }); + var customers = await _customerService.GetAllAsync(); + return Ok(customers.Select(MapToDto)); } [HttpGet("{id}")] - public IActionResult GetById(int id) + [ProducesResponseType(typeof(CustomerDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task GetById(int id) { - // TODO: Implement — migrate logic from monolith - return Ok(new { service = "Customer", id }); + var customer = await _customerService.GetByIdAsync(id); + if (customer is null) return NotFound(); + return Ok(MapToDto(customer)); } + + [HttpPost] + [ProducesResponseType(typeof(CustomerDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Create([FromBody] CustomerDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Name) || string.IsNullOrWhiteSpace(dto.Email)) + return BadRequest("Name and Email are required."); + + var customer = new Entities.Customer + { + Name = dto.Name, + Email = dto.Email, + PhoneNumber = dto.PhoneNumber, + Address = dto.Address, + City = dto.City, + Gender = Enum.TryParse(dto.Gender, true, out var g) ? g : Gender.None + }; + + var created = await _customerService.CreateAsync(customer); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, MapToDto(created)); + } + + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Update(int id, [FromBody] CustomerDto dto) + { + var existing = await _customerService.GetByIdAsync(id); + if (existing is null) return NotFound(); + + existing.Name = dto.Name ?? existing.Name; + existing.Email = dto.Email ?? existing.Email; + existing.PhoneNumber = dto.PhoneNumber; + existing.Address = dto.Address; + existing.City = dto.City; + existing.Gender = Enum.TryParse(dto.Gender, true, out var g) ? g : existing.Gender; + + await _customerService.UpdateAsync(existing); + return NoContent(); + } + + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Delete(int id) + { + var deleted = await _customerService.DeleteAsync(id); + if (!deleted) return NotFound(); + return NoContent(); + } + + private static CustomerDto MapToDto(Entities.Customer c) => new() + { + Id = c.Id, + Name = c.Name, + Email = c.Email, + PhoneNumber = c.PhoneNumber, + Address = c.Address, + City = c.City, + Gender = c.Gender.ToString() + }; } diff --git a/src/Services/Customer/Customer.API/Customer.API.csproj b/src/Services/Customer/Customer.API/Customer.API.csproj index 8c284d4..f13a86a 100644 --- a/src/Services/Customer/Customer.API/Customer.API.csproj +++ b/src/Services/Customer/Customer.API/Customer.API.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Services/Customer/Customer.API/DTOs/CustomerDto.cs b/src/Services/Customer/Customer.API/DTOs/CustomerDto.cs new file mode 100644 index 0000000..c5ae875 --- /dev/null +++ b/src/Services/Customer/Customer.API/DTOs/CustomerDto.cs @@ -0,0 +1,12 @@ +namespace Customer.API.DTOs; + +public class CustomerDto +{ + public int Id { get; set; } + public string? Name { get; set; } + public string? Email { get; set; } + public string? PhoneNumber { get; set; } + public string? Address { get; set; } + public string? City { get; set; } + public string? Gender { get; set; } +} diff --git a/src/Services/Customer/Customer.API/Program.cs b/src/Services/Customer/Customer.API/Program.cs index 2cee388..e4d2198 100644 --- a/src/Services/Customer/Customer.API/Program.cs +++ b/src/Services/Customer/Customer.API/Program.cs @@ -1,4 +1,9 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; using Customer.Infrastructure.Data; +using Customer.Domain.Interfaces; +using Customer.Infrastructure.Services; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -11,6 +16,31 @@ builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); +builder.Services.AddScoped(); + +// 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 +55,9 @@ app.UseSwaggerUI(); } +app.UseAuthentication(); +app.UseAuthorization(); + app.MapControllers(); app.MapHealthChecks("/healthz"); diff --git a/src/Services/Customer/Customer.API/appsettings.json b/src/Services/Customer/Customer.API/appsettings.json index 3d9557b..cbf3b05 100644 --- a/src/Services/Customer/Customer.API/appsettings.json +++ b/src/Services/Customer/Customer.API/appsettings.json @@ -7,5 +7,11 @@ }, "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=customerdb;Username=postgres;Password=postgres" + }, + "Jwt": { + "Secret": "QuickApp_Microservices_SuperSecret_Key_For_Dev_Only_Min_32_Chars!", + "Issuer": "quickapp-identity", + "Audience": "quickapp-api", + "ExpirationMinutes": 60 } } diff --git a/src/Services/Customer/Customer.Infrastructure/Services/.gitkeep b/src/Services/Customer/Customer.Infrastructure/Services/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Services/Customer/Customer.Infrastructure/Services/CustomerService.cs b/src/Services/Customer/Customer.Infrastructure/Services/CustomerService.cs new file mode 100644 index 0000000..8df69ab --- /dev/null +++ b/src/Services/Customer/Customer.Infrastructure/Services/CustomerService.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore; +using Customer.Domain.Interfaces; +using Customer.Infrastructure.Data; +using Entities = Customer.Domain.Entities; + +namespace Customer.Infrastructure.Services; + +public class CustomerService : ICustomerService +{ + private readonly CustomerDbContext _context; + + public CustomerService(CustomerDbContext context) + { + _context = context; + } + + public async Task> GetAllAsync() + { + return await _context.Customers.OrderBy(c => c.Name).ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + return await _context.Customers.FindAsync(id); + } + + public async Task CreateAsync(Entities.Customer customer) + { + customer.CreatedDate = DateTime.UtcNow; + customer.UpdatedDate = DateTime.UtcNow; + _context.Customers.Add(customer); + await _context.SaveChangesAsync(); + return customer; + } + + public async Task UpdateAsync(Entities.Customer customer) + { + customer.UpdatedDate = DateTime.UtcNow; + _context.Customers.Update(customer); + await _context.SaveChangesAsync(); + } + + public async Task DeleteAsync(int id) + { + var customer = await _context.Customers.FindAsync(id); + if (customer == null) return false; + _context.Customers.Remove(customer); + await _context.SaveChangesAsync(); + return true; + } +}