From fdca61e43b9d902049849c222d9c2cfb0a036ef3 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:38:00 +0000 Subject: [PATCH 1/5] Customer service: port entity/persistence from monolith, JWT auth, full CRUD --- .../Controllers/CustomerController.cs | 136 ++++++++++++++++-- .../Customer/Customer.API/Customer.API.csproj | 3 +- src/Services/Customer/Customer.API/Dockerfile | 2 - src/Services/Customer/Customer.API/Program.cs | 71 ++++++++- .../Customer.API/ViewModels/CustomerVM.cs | 12 ++ .../Customer/Customer.API/appsettings.json | 5 + .../Customer.Domain/Entities/.gitkeep | 0 .../Customer.Domain/Entities/BaseEntity.cs | 18 +++ .../Customer.Domain/Entities/Customer.cs | 11 ++ .../Customer.Domain/Entities/Gender.cs | 8 ++ .../Data/CustomerDbContext.cs | 11 +- src/Services/Customer/e2e-smoke.sh | 27 ++++ 12 files changed, 288 insertions(+), 16 deletions(-) create mode 100644 src/Services/Customer/Customer.API/ViewModels/CustomerVM.cs delete mode 100644 src/Services/Customer/Customer.Domain/Entities/.gitkeep create mode 100644 src/Services/Customer/Customer.Domain/Entities/BaseEntity.cs create mode 100644 src/Services/Customer/Customer.Domain/Entities/Customer.cs create mode 100644 src/Services/Customer/Customer.Domain/Entities/Gender.cs create mode 100755 src/Services/Customer/e2e-smoke.sh diff --git a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs index 06cfead..7788224 100644 --- a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs +++ b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs @@ -1,29 +1,145 @@ +using Customer.API.ViewModels; +using Customer.Infrastructure.Data; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using CustomerEntity = Customer.Domain.Entities.Customer; +using GenderEnum = Customer.Domain.Entities.Gender; namespace Customer.API.Controllers; [ApiController] -[Route("api/[controller]")] +[Authorize] +[Produces("application/json")] public class CustomerController : ControllerBase { + private readonly CustomerDbContext _dbContext; private readonly ILogger _logger; - public CustomerController(ILogger logger) + public CustomerController(CustomerDbContext dbContext, ILogger logger) { + _dbContext = dbContext; _logger = logger; } - [HttpGet] - public IActionResult GetAll() + // External: GET /api/customers -> service GET / + [HttpGet("/")] + [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 _dbContext.Customers + .OrderBy(c => c.Name) + .ToListAsync(); + + return Ok(customers.Select(ToViewModel)); } - [HttpGet("{id}")] - public IActionResult GetById(int id) + // External: GET /api/customers/{id} -> service GET /{id} + [HttpGet("/{id:int}")] + [ProducesResponseType(typeof(CustomerVM), 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 _dbContext.Customers.FindAsync(id); + if (customer == null) + return NotFound(); + + return Ok(ToViewModel(customer)); } + + // External: POST /api/customers -> service POST / + [HttpPost("/")] + [ProducesResponseType(typeof(CustomerVM), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task> Create([FromBody] CustomerVM model) + { + if (string.IsNullOrWhiteSpace(model.Name)) + ModelState.AddModelError(nameof(model.Name), "Customer name cannot be empty"); + if (string.IsNullOrWhiteSpace(model.Gender)) + ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty"); + if (!ModelState.IsValid) + return BadRequest(ModelState); + + var customer = new CustomerEntity + { + Name = model.Name!, + Email = model.Email!, + PhoneNumber = model.PhoneNumber, + Address = model.Address, + City = model.City, + Gender = ParseGender(model.Gender), + CreatedDate = DateTime.UtcNow, + UpdatedDate = DateTime.UtcNow + }; + + _dbContext.Customers.Add(customer); + await _dbContext.SaveChangesAsync(); + + var vm = ToViewModel(customer); + return CreatedAtAction(nameof(GetById), new { id = customer.Id }, vm); + } + + // External: PUT /api/customers/{id} -> service PUT /{id} + [HttpPut("/{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Update(int id, [FromBody] CustomerVM model) + { + var customer = await _dbContext.Customers.FindAsync(id); + if (customer == null) + return NotFound(); + + if (string.IsNullOrWhiteSpace(model.Name)) + ModelState.AddModelError(nameof(model.Name), "Customer name cannot be empty"); + if (string.IsNullOrWhiteSpace(model.Gender)) + ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty"); + if (!ModelState.IsValid) + return BadRequest(ModelState); + + customer.Name = model.Name!; + customer.Email = model.Email!; + customer.PhoneNumber = model.PhoneNumber; + customer.Address = model.Address; + customer.City = model.City; + customer.Gender = ParseGender(model.Gender); + customer.UpdatedDate = DateTime.UtcNow; + + await _dbContext.SaveChangesAsync(); + return NoContent(); + } + + // External: DELETE /api/customers/{id} -> service DELETE /{id} + [HttpDelete("/{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Delete(int id) + { + var customer = await _dbContext.Customers.FindAsync(id); + if (customer == null) + return NotFound(); + + _dbContext.Customers.Remove(customer); + await _dbContext.SaveChangesAsync(); + return NoContent(); + } + + private static CustomerVM ToViewModel(CustomerEntity c) => new() + { + Id = c.Id, + Name = c.Name, + Email = c.Email, + PhoneNumber = c.PhoneNumber, + Address = c.Address, + City = c.City, + Gender = c.Gender.ToString() + }; + + private static GenderEnum ParseGender(string? value) => + Enum.TryParse(value, ignoreCase: true, out var gender) ? gender : GenderEnum.None; } diff --git a/src/Services/Customer/Customer.API/Customer.API.csproj b/src/Services/Customer/Customer.API/Customer.API.csproj index 8c284d4..d1704ea 100644 --- a/src/Services/Customer/Customer.API/Customer.API.csproj +++ b/src/Services/Customer/Customer.API/Customer.API.csproj @@ -7,10 +7,9 @@ - - + diff --git a/src/Services/Customer/Customer.API/Dockerfile b/src/Services/Customer/Customer.API/Dockerfile index 2fc64ce..f6c68ab 100644 --- a/src/Services/Customer/Customer.API/Dockerfile +++ b/src/Services/Customer/Customer.API/Dockerfile @@ -7,8 +7,6 @@ WORKDIR /src COPY ["Services/Customer/Customer.API/Customer.API.csproj", "Services/Customer/Customer.API/"] COPY ["Services/Customer/Customer.Domain/Customer.Domain.csproj", "Services/Customer/Customer.Domain/"] COPY ["Services/Customer/Customer.Infrastructure/Customer.Infrastructure.csproj", "Services/Customer/Customer.Infrastructure/"] -COPY ["Shared/Shared.Contracts/Shared.Contracts.csproj", "Shared/Shared.Contracts/"] -COPY ["Shared/Shared.Infrastructure/Shared.Infrastructure.csproj", "Shared/Shared.Infrastructure/"] RUN dotnet restore "Services/Customer/Customer.API/Customer.API.csproj" COPY . . WORKDIR "/src/Services/Customer/Customer.API" diff --git a/src/Services/Customer/Customer.API/Program.cs b/src/Services/Customer/Customer.API/Program.cs index e46940a..5fc742b 100644 --- a/src/Services/Customer/Customer.API/Program.cs +++ b/src/Services/Customer/Customer.API/Program.cs @@ -1,24 +1,93 @@ +using System.Text; using Customer.Infrastructure.Data; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "Enter the JWT bearer token issued by the Identity service." + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } + }, + Array.Empty() + } + }); +}); builder.Services.AddHealthChecks(); builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); +var jwtSection = builder.Configuration.GetSection("Jwt"); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.MapInboundClaims = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSection["Issuer"], + ValidAudience = jwtSection["Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSection["Key"]!)), + NameClaimType = "name", + RoleClaimType = "role" + }; + }); +builder.Services.AddAuthorization(); + var app = builder.Build(); +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("Startup"); + + const int maxAttempts = 12; + for (var attempt = 1; ; attempt++) + { + try + { + db.Database.EnsureCreated(); + break; + } + catch (Exception ex) when (attempt < maxAttempts) + { + logger.LogWarning(ex, "Database not ready (attempt {Attempt}/{Max}); retrying in 5s...", attempt, maxAttempts); + Thread.Sleep(TimeSpan.FromSeconds(5)); + } + } +} + if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } +app.UseAuthentication(); +app.UseAuthorization(); + app.MapControllers(); app.MapHealthChecks("/healthz"); diff --git a/src/Services/Customer/Customer.API/ViewModels/CustomerVM.cs b/src/Services/Customer/Customer.API/ViewModels/CustomerVM.cs new file mode 100644 index 0000000..03f806b --- /dev/null +++ b/src/Services/Customer/Customer.API/ViewModels/CustomerVM.cs @@ -0,0 +1,12 @@ +namespace Customer.API.ViewModels; + +public class CustomerVM +{ + 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/appsettings.json b/src/Services/Customer/Customer.API/appsettings.json index 3d9557b..9c99fb6 100644 --- a/src/Services/Customer/Customer.API/appsettings.json +++ b/src/Services/Customer/Customer.API/appsettings.json @@ -7,5 +7,10 @@ }, "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=customerdb;Username=postgres;Password=postgres" + }, + "Jwt": { + "Key": "quickapp-dev-only-signing-key-change-me-please-32+chars", + "Issuer": "quickapp-identity", + "Audience": "quickapp" } } diff --git a/src/Services/Customer/Customer.Domain/Entities/.gitkeep b/src/Services/Customer/Customer.Domain/Entities/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Services/Customer/Customer.Domain/Entities/BaseEntity.cs b/src/Services/Customer/Customer.Domain/Entities/BaseEntity.cs new file mode 100644 index 0000000..eb246b3 --- /dev/null +++ b/src/Services/Customer/Customer.Domain/Entities/BaseEntity.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Customer.Domain.Entities; + +public class BaseEntity +{ + public int Id { get; set; } + + [MaxLength(40)] + public string? CreatedBy { get; set; } + + [MaxLength(40)] + public string? UpdatedBy { get; set; } + + public DateTime UpdatedDate { get; set; } + + public DateTime CreatedDate { get; set; } +} diff --git a/src/Services/Customer/Customer.Domain/Entities/Customer.cs b/src/Services/Customer/Customer.Domain/Entities/Customer.cs new file mode 100644 index 0000000..c91b01a --- /dev/null +++ b/src/Services/Customer/Customer.Domain/Entities/Customer.cs @@ -0,0 +1,11 @@ +namespace Customer.Domain.Entities; + +public class Customer : BaseEntity +{ + public required string Name { get; set; } + public required string Email { get; set; } + public string? PhoneNumber { get; set; } + public string? Address { get; set; } + public string? City { get; set; } + public Gender Gender { get; set; } +} diff --git a/src/Services/Customer/Customer.Domain/Entities/Gender.cs b/src/Services/Customer/Customer.Domain/Entities/Gender.cs new file mode 100644 index 0000000..1901e7c --- /dev/null +++ b/src/Services/Customer/Customer.Domain/Entities/Gender.cs @@ -0,0 +1,8 @@ +namespace Customer.Domain.Entities; + +public enum Gender +{ + None, + Female, + Male +} diff --git a/src/Services/Customer/Customer.Infrastructure/Data/CustomerDbContext.cs b/src/Services/Customer/Customer.Infrastructure/Data/CustomerDbContext.cs index c986f1e..69785ab 100644 --- a/src/Services/Customer/Customer.Infrastructure/Data/CustomerDbContext.cs +++ b/src/Services/Customer/Customer.Infrastructure/Data/CustomerDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using CustomerEntity = Customer.Domain.Entities.Customer; namespace Customer.Infrastructure.Data; @@ -8,9 +9,17 @@ public CustomerDbContext(DbContextOptions options) : base(opt { } + public DbSet Customers => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - // TODO: Configure entity mappings migrated from monolith + + modelBuilder.Entity().Property(c => c.Name).IsRequired().HasMaxLength(100); + modelBuilder.Entity().HasIndex(c => c.Name); + modelBuilder.Entity().Property(c => c.Email).HasMaxLength(100); + modelBuilder.Entity().Property(c => c.PhoneNumber).IsUnicode(false).HasMaxLength(30); + modelBuilder.Entity().Property(c => c.City).HasMaxLength(50); + modelBuilder.Entity().ToTable("AppCustomers"); } } diff --git a/src/Services/Customer/e2e-smoke.sh b/src/Services/Customer/e2e-smoke.sh new file mode 100755 index 0000000..257aaf6 --- /dev/null +++ b/src/Services/Customer/e2e-smoke.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# E2E smoke test for the Customer service, exercised THROUGH the API gateway. +# Asserts: +# 1. GET /api/customers/healthz -> 200 (service is up via gateway) +# 2. GET /api/customers (no Authorization) -> 401 (every endpoint is [Authorize]) +# Exits non-zero on any failure. +set -euo pipefail + +GATEWAY_URL="${GATEWAY_URL:-http://localhost:5000}" +HEALTH_PATH="/api/customers/healthz" +RESOURCE_PATH="/api/customers" + +fail() { echo "SMOKE FAIL: $*" >&2; exit 1; } + +echo "== Customer service E2E smoke (via gateway ${GATEWAY_URL}) ==" + +echo "-- [1/2] health check: GET ${HEALTH_PATH} (expect 200)" +health_code="$(curl -s -o /dev/null -w '%{http_code}' "${GATEWAY_URL}${HEALTH_PATH}")" +[ "${health_code}" = "200" ] || fail "health expected 200, got ${health_code}" +echo " OK: ${health_code}" + +echo "-- [2/2] unauthenticated access: GET ${RESOURCE_PATH} (expect 401)" +unauth_code="$(curl -s -o /dev/null -w '%{http_code}' "${GATEWAY_URL}${RESOURCE_PATH}")" +[ "${unauth_code}" = "401" ] || fail "unauth expected 401, got ${unauth_code}" +echo " OK: ${unauth_code}" + +echo "== SMOKE PASS: health 200 + unauth 401 ==" From 6384f31d1b2c4127266006dcc4ef6d5b9ff5c5c9 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:42:37 +0000 Subject: [PATCH 2/5] Validate Email in Create/Update (return 400 instead of 500 on missing email) --- .../Customer/Customer.API/Controllers/CustomerController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs index 7788224..197f572 100644 --- a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs +++ b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs @@ -58,6 +58,8 @@ public async Task> Create([FromBody] CustomerVM model) { if (string.IsNullOrWhiteSpace(model.Name)) ModelState.AddModelError(nameof(model.Name), "Customer name cannot be empty"); + if (string.IsNullOrWhiteSpace(model.Email)) + ModelState.AddModelError(nameof(model.Email), "Email cannot be empty"); if (string.IsNullOrWhiteSpace(model.Gender)) ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty"); if (!ModelState.IsValid) @@ -96,6 +98,8 @@ public async Task Update(int id, [FromBody] CustomerVM model) if (string.IsNullOrWhiteSpace(model.Name)) ModelState.AddModelError(nameof(model.Name), "Customer name cannot be empty"); + if (string.IsNullOrWhiteSpace(model.Email)) + ModelState.AddModelError(nameof(model.Email), "Email cannot be empty"); if (string.IsNullOrWhiteSpace(model.Gender)) ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty"); if (!ModelState.IsValid) From 9c24b436d80472bdfd087b28694ff9b3eea40bba 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:48:53 +0000 Subject: [PATCH 3/5] Reject unrecognized Gender values with 400 instead of silently storing None --- .../Customer/Customer.API/Controllers/CustomerController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs index 197f572..da2944c 100644 --- a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs +++ b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs @@ -62,6 +62,8 @@ public async Task> Create([FromBody] CustomerVM model) ModelState.AddModelError(nameof(model.Email), "Email cannot be empty"); if (string.IsNullOrWhiteSpace(model.Gender)) ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty"); + else if (!Enum.TryParse(model.Gender, ignoreCase: true, out _)) + ModelState.AddModelError(nameof(model.Gender), "Invalid gender value"); if (!ModelState.IsValid) return BadRequest(ModelState); @@ -102,6 +104,8 @@ public async Task Update(int id, [FromBody] CustomerVM model) ModelState.AddModelError(nameof(model.Email), "Email cannot be empty"); if (string.IsNullOrWhiteSpace(model.Gender)) ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty"); + else if (!Enum.TryParse(model.Gender, ignoreCase: true, out _)) + ModelState.AddModelError(nameof(model.Gender), "Invalid gender value"); if (!ModelState.IsValid) return BadRequest(ModelState); From 653e304896f0248335b84f845a9cb94fcea7929f 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:55:13 +0000 Subject: [PATCH 4/5] Reject out-of-range numeric Gender values via Enum.IsDefined --- .../Customer/Customer.API/Controllers/CustomerController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs index da2944c..65fdc4c 100644 --- a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs +++ b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs @@ -62,7 +62,7 @@ public async Task> Create([FromBody] CustomerVM model) ModelState.AddModelError(nameof(model.Email), "Email cannot be empty"); if (string.IsNullOrWhiteSpace(model.Gender)) ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty"); - else if (!Enum.TryParse(model.Gender, ignoreCase: true, out _)) + else if (!Enum.TryParse(model.Gender, ignoreCase: true, out var parsedGender) || !Enum.IsDefined(parsedGender)) ModelState.AddModelError(nameof(model.Gender), "Invalid gender value"); if (!ModelState.IsValid) return BadRequest(ModelState); @@ -104,7 +104,7 @@ public async Task Update(int id, [FromBody] CustomerVM model) ModelState.AddModelError(nameof(model.Email), "Email cannot be empty"); if (string.IsNullOrWhiteSpace(model.Gender)) ModelState.AddModelError(nameof(model.Gender), "Gender cannot be empty"); - else if (!Enum.TryParse(model.Gender, ignoreCase: true, out _)) + else if (!Enum.TryParse(model.Gender, ignoreCase: true, out var parsedGender) || !Enum.IsDefined(parsedGender)) ModelState.AddModelError(nameof(model.Gender), "Invalid gender value"); if (!ModelState.IsValid) return BadRequest(ModelState); From 79d24262ac6a5e865f550deed2579b2569e3849c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 05:05:44 +0000 Subject: [PATCH 5/5] Populate CreatedBy/UpdatedBy audit fields from authenticated JWT identity --- .../Customer.API/Controllers/CustomerController.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs index 65fdc4c..ef83dc2 100644 --- a/src/Services/Customer/Customer.API/Controllers/CustomerController.cs +++ b/src/Services/Customer/Customer.API/Controllers/CustomerController.cs @@ -75,6 +75,8 @@ public async Task> Create([FromBody] CustomerVM model) Address = model.Address, City = model.City, Gender = ParseGender(model.Gender), + CreatedBy = CurrentUser(), + UpdatedBy = CurrentUser(), CreatedDate = DateTime.UtcNow, UpdatedDate = DateTime.UtcNow }; @@ -115,6 +117,7 @@ public async Task Update(int id, [FromBody] CustomerVM model) customer.Address = model.Address; customer.City = model.City; customer.Gender = ParseGender(model.Gender); + customer.UpdatedBy = CurrentUser(); customer.UpdatedDate = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); @@ -150,4 +153,13 @@ public async Task Delete(int id) private static GenderEnum ParseGender(string? value) => Enum.TryParse(value, ignoreCase: true, out var gender) ? gender : GenderEnum.None; + + // CreatedBy/UpdatedBy column is varchar(40); truncate the JWT identity to fit. + private string? CurrentUser() + { + var name = User.Identity?.Name; + if (string.IsNullOrEmpty(name)) + return null; + return name.Length > 40 ? name[..40] : name; + } }