From 028ef4c1cdc2d9e521ee48e10ec96bccacb5ef04 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 4 Mar 2026 13:08:08 +0700 Subject: [PATCH] feat: implement user-based wallet and transaction retrieval by parsing JWT sub claim and adjust JWT validation parameters across services. --- .../Controllers/FinancialController.cs | 106 ++++++++++++++++-- deployments/local/docker-compose.yml | 5 + .../src/MembershipService.API/Program.cs | 7 +- .../MemberEntityTypeConfiguration.cs | 5 +- .../MembershipServiceContext.cs | 4 + .../src/WalletService.API/Program.cs | 4 +- 6 files changed, 115 insertions(+), 16 deletions(-) diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs index 9ceca259..64f9dca8 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs @@ -22,28 +22,112 @@ public class FinancialController : ControllerBase } /// - /// EN: Get wallets for the current merchant. - /// VI: Lấy ví của merchant hiện tại. + /// EN: Extract userId from JWT Bearer token in the Authorization header. + /// VI: Trích xuất userId từ JWT Bearer token trong header Authorization. + /// + private Guid? GetUserIdFromToken() + { + var authHeader = Request.Headers["Authorization"].FirstOrDefault(); + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) return null; + + var token = authHeader["Bearer ".Length..]; + var parts = token.Split('.'); + if (parts.Length != 3) return null; + + var payload = parts[1]; + // EN: Fix base64url padding / VI: Sửa padding base64url + switch (payload.Length % 4) + { + case 2: payload += "=="; break; + case 3: payload += "="; break; + } + payload = payload.Replace('-', '+').Replace('_', '/'); + + try + { + var json = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payload)); + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("sub", out var sub) && Guid.TryParse(sub.GetString(), out var userId)) + return userId; + } + catch { /* invalid token format */ } + + return null; + } + + /// + /// EN: Get wallet for the current user (extracted from JWT sub claim). + /// VI: Lấy ví của user hiện tại (trích từ JWT sub claim). /// [HttpGet("wallets")] - public Task GetWallets() => - _wallet.GetAsync("/api/v1/wallets").ProxyAsync(); + public async Task GetWallets() + { + var userId = GetUserIdFromToken(); + if (userId == null) + return Unauthorized(new { message = "Cannot extract user ID from token" }); + + // EN: WalletService returns single wallet; wrap in array for frontend compatibility. + // VI: WalletService trả về 1 ví; bọc trong array cho tương thích frontend. + var response = await _wallet.GetAsync($"/api/v1/wallets/{userId}"); + var content = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + // EN: If 404, return empty array (user has no wallet yet). + // VI: Nếu 404, trả array rỗng (user chưa có ví). + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + return new ContentResult { StatusCode = 200, Content = "[]", ContentType = "application/json" }; + + return new ContentResult + { + StatusCode = (int)response.StatusCode, + Content = content, + ContentType = "application/json" + }; + } + + // EN: Extract wallet data from ApiResponse envelope and wrap in array. + // VI: Trích dữ liệu ví từ ApiResponse envelope và bọc trong array. + try + { + using var doc = JsonDocument.Parse(content); + if (doc.RootElement.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Object) + return new ContentResult { StatusCode = 200, Content = $"[{data.GetRawText()}]", ContentType = "application/json" }; + } + catch { /* fallback */ } + + return new ContentResult { StatusCode = 200, Content = $"[{content}]", ContentType = "application/json" }; + } /// - /// EN: Get wallet transactions for the current merchant. - /// VI: Lấy giao dịch ví của merchant hiện tại. + /// EN: Get wallet transactions for the current user. + /// VI: Lấy giao dịch ví của user hiện tại. /// [HttpGet("wallet/transactions")] - public Task GetWalletTransactions([FromQuery] int limit = 50) => - _wallet.GetAsync($"/api/v1/wallet/transactions?limit={limit}").ProxyAsync(); + public async Task GetWalletTransactions([FromQuery] int limit = 50) + { + var userId = GetUserIdFromToken(); + if (userId == null) + return Unauthorized(new { message = "Cannot extract user ID from token" }); + + var response = await _wallet.GetAsync($"/api/v1/wallets/{userId}/transactions?limit={limit}"); + + // EN: If wallet not found, return empty array (user has no wallet yet). + // VI: Nếu ví không tồn tại, trả array rỗng (user chưa có ví). + if (!response.IsSuccessStatusCode) + return new ContentResult { StatusCode = 200, Content = "[]", ContentType = "application/json" }; + + var content = await response.Content.ReadAsStringAsync(); + return new ContentResult { StatusCode = 200, Content = content, ContentType = "application/json" }; + } /// - /// EN: Get campaigns for current merchant. - /// VI: Lấy danh sách chiến dịch của merchant hiện tại. + /// EN: Get promotions for current merchant. + /// VI: Lấy danh sách khuyến mãi của merchant hiện tại. /// [HttpGet("promotions")] public Task GetPromotions() => - _promotion.GetAsync("/api/v1/promotions").ProxyAsync(); + _promotion.GetAsync("/api/v1/campaigns").ProxyAsync(); /// /// EN: Get campaigns for current merchant. diff --git a/deployments/local/docker-compose.yml b/deployments/local/docker-compose.yml index 5d1d2fab..bf605be0 100644 --- a/deployments/local/docker-compose.yml +++ b/deployments/local/docker-compose.yml @@ -212,6 +212,11 @@ services: # VI: Giao tiếp IAM Service - IamService__BaseUrl=http://iam-service-net:8080 - IamService__ServiceName=membership-service + # EN: JWT Configuration + # VI: Cấu hình JWT + - Jwt__Authority=http://iam-service-net:8080 + - Jwt__Audience=goodgo-api + - Jwt__RequireHttpsMetadata=false ports: - "5003:8080" depends_on: diff --git a/services/membership-service-net/src/MembershipService.API/Program.cs b/services/membership-service-net/src/MembershipService.API/Program.cs index 05fd84c2..d9cba3dd 100644 --- a/services/membership-service-net/src/MembershipService.API/Program.cs +++ b/services/membership-service-net/src/MembershipService.API/Program.cs @@ -137,8 +137,13 @@ try .AddJwtBearer("Bearer", options => { options.Authority = builder.Configuration["Jwt:Authority"] ?? "http://localhost:5001"; - options.Audience = builder.Configuration["Jwt:Audience"] ?? "membership-service"; options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + }; }); builder.Services.AddAuthorization(); diff --git a/services/membership-service-net/src/MembershipService.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs b/services/membership-service-net/src/MembershipService.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs index ab49c8b5..e1d20d61 100644 --- a/services/membership-service-net/src/MembershipService.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs +++ b/services/membership-service-net/src/MembershipService.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs @@ -74,7 +74,8 @@ public class MemberEntityTypeConfiguration : IEntityTypeConfiguration // EN: Soft delete // VI: Xóa mềm - builder.Property("_isDeleted") + builder.Property(m => m.IsDeleted) + .HasField("_isDeleted") .HasColumnName("is_deleted") .HasDefaultValue(false); @@ -93,7 +94,7 @@ public class MemberEntityTypeConfiguration : IEntityTypeConfiguration builder.HasIndex("_currentExp") .HasDatabaseName("ix_members_current_exp"); - builder.HasIndex("_isDeleted") + builder.HasIndex(m => m.IsDeleted) .HasDatabaseName("ix_members_is_deleted"); // EN: Ignore domain events (not persisted) diff --git a/services/membership-service-net/src/MembershipService.Infrastructure/MembershipServiceContext.cs b/services/membership-service-net/src/MembershipService.Infrastructure/MembershipServiceContext.cs index d97d4ca9..a5e048fb 100644 --- a/services/membership-service-net/src/MembershipService.Infrastructure/MembershipServiceContext.cs +++ b/services/membership-service-net/src/MembershipService.Infrastructure/MembershipServiceContext.cs @@ -63,6 +63,10 @@ public class MembershipServiceContext : DbContext, IUnitOfWork { base.OnModelCreating(modelBuilder); + // EN: Ignore Enumeration types to prevent EF Core discovery issues + // VI: Bỏ qua Enumeration types để tránh lỗi EF Core discovery + modelBuilder.Ignore(); + // EN: Apply entity configurations // VI: Áp dụng entity configurations modelBuilder.ApplyConfigurationsFromAssembly(typeof(MembershipServiceContext).Assembly); diff --git a/services/wallet-service-net/src/WalletService.API/Program.cs b/services/wallet-service-net/src/WalletService.API/Program.cs index cf9cb504..e5054e3b 100644 --- a/services/wallet-service-net/src/WalletService.API/Program.cs +++ b/services/wallet-service-net/src/WalletService.API/Program.cs @@ -115,8 +115,8 @@ try options.TokenValidationParameters = new() { ValidateAudience = false, - ValidateIssuer = true, - ValidIssuer = builder.Configuration["Jwt:Issuer"] + ValidateIssuer = false, + ValidateLifetime = true, }; });