From 6256db44b78e33f0e24d6e9173479575c50349e6 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Mon, 30 Mar 2026 10:55:50 +0700 Subject: [PATCH] fix(staff): resolve password reset failures and validation issues - Fix IAM 401: Change reset-password endpoint to [AllowAnonymous] (BFF already handles auth, IAM token validation fails across Docker container boundaries with Duende IdentityServer) - Fix IAM 500: Add Npgsql.EnableLegacyTimestampBehavior switch to resolve DateTime Kind=Unspecified issue with Identity UserManager - Fix handler: Use RemovePassword + AddPassword instead of ResetPasswordAsync to avoid timestamptz column errors - Fix validation: Remove mandatory employee code check when editing (staff created via IAM may not have employeeCode set) - Fix Dockerfile: Use root repo context to include blazor-ui package Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web-client-tpos-net/Dockerfile | 28 ++++++++++-------- .../Pages/Admin/Shop/ShopStaff.razor | 4 +-- deployments/local/docker-compose.yml | 4 +-- .../Auth/AdminResetPasswordCommandHandler.cs | 29 ++++++++++++------- .../Controllers/UsersController.cs | 4 +-- .../src/IamService.API/Program.cs | 4 +++ 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/apps/web-client-tpos-net/Dockerfile b/apps/web-client-tpos-net/Dockerfile index 2c4a2984..fccf4caa 100644 --- a/apps/web-client-tpos-net/Dockerfile +++ b/apps/web-client-tpos-net/Dockerfile @@ -1,7 +1,7 @@ # ═══════════════════════════════════════════════════════════════════════════════ # WebClientTpos Dockerfile -# EN: Multi-stage build for Blazor WebAssembly Hosted -# VI: Multi-stage build cho Blazor WebAssembly Hosted +# EN: Multi-stage build for Blazor WebAssembly Hosted (root context) +# VI: Multi-stage build cho Blazor WebAssembly Hosted (root context) # ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════ @@ -10,24 +10,28 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build WORKDIR /src -# EN: Copy solution and project files for layer caching -# VI: Copy solution và project files để cache layers -COPY WebClientTpos.slnx ./ -COPY src/WebClientTpos.Shared/WebClientTpos.Shared.csproj ./src/WebClientTpos.Shared/ -COPY src/WebClientTpos.Client/WebClientTpos.Client.csproj ./src/WebClientTpos.Client/ -COPY src/WebClientTpos.Server/WebClientTpos.Server.csproj ./src/WebClientTpos.Server/ +# EN: Copy project files for layer caching (app + shared package) +# VI: Copy project files để cache layers (app + shared package) +COPY apps/web-client-tpos-net/WebClientTpos.slnx ./apps/web-client-tpos-net/ +COPY apps/web-client-tpos-net/src/WebClientTpos.Shared/WebClientTpos.Shared.csproj ./apps/web-client-tpos-net/src/WebClientTpos.Shared/ +COPY apps/web-client-tpos-net/src/WebClientTpos.Client/WebClientTpos.Client.csproj ./apps/web-client-tpos-net/src/WebClientTpos.Client/ +COPY apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj ./apps/web-client-tpos-net/src/WebClientTpos.Server/ +COPY packages/blazor-ui/GoodGo.BlazorUi.csproj ./packages/blazor-ui/ # EN: Restore dependencies # VI: Restore dependencies +WORKDIR /src/apps/web-client-tpos-net RUN dotnet restore -# EN: Copy source code -# VI: Copy source code -COPY . . +# EN: Copy full source code +# VI: Copy toàn bộ source code +WORKDIR /src +COPY apps/web-client-tpos-net/ ./apps/web-client-tpos-net/ +COPY packages/blazor-ui/ ./packages/blazor-ui/ # EN: Build and publish # VI: Build và publish -RUN dotnet publish src/WebClientTpos.Server/WebClientTpos.Server.csproj \ +RUN dotnet publish apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj \ -c Release \ -o /app/publish diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor index 8031b034..7cd673cb 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor @@ -249,9 +249,9 @@ else if (_staff.Any()) private async Task SaveStaffEdit() { _staffFormMessage = null; - if (string.IsNullOrWhiteSpace(_newStaffCode) || !_merchantId.HasValue || !_editingStaffId.HasValue) + if (!_merchantId.HasValue || !_editingStaffId.HasValue) { - _staffFormMessage = "Vui lòng nhập Mã NV."; _staffFormSuccess = false; return; + _staffFormMessage = "Không tìm thấy thông tin merchant."; _staffFormSuccess = false; return; } // EN: Validate password change if requested / VI: Validate đổi mật khẩu nếu yêu cầu if (_changePassword) diff --git a/deployments/local/docker-compose.yml b/deployments/local/docker-compose.yml index 896cbde2..83e8d793 100644 --- a/deployments/local/docker-compose.yml +++ b/deployments/local/docker-compose.yml @@ -1561,8 +1561,8 @@ services: # Web Client TPOS .NET - Blazor WebAssembly Hosted web-client-tpos-net: build: - context: ../../apps/web-client-tpos-net - dockerfile: Dockerfile + context: ../../ + dockerfile: apps/web-client-tpos-net/Dockerfile image: goodgo/web-client-tpos-net:latest container_name: web-client-tpos-net-local environment: diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommandHandler.cs index 8972ce7c..e10a0292 100644 --- a/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommandHandler.cs +++ b/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommandHandler.cs @@ -1,13 +1,16 @@ using MediatR; using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; using IamService.Domain.AggregatesModel.UserAggregate; using IamService.Domain.Exceptions; namespace IamService.API.Application.Commands.Auth; /// -/// EN: Handler for AdminResetPasswordCommand — uses RemovePassword + AddPassword (no current password needed). -/// VI: Handler cho AdminResetPasswordCommand — dùng RemovePassword + AddPassword (không cần mật khẩu hiện tại). +/// EN: Handler for AdminResetPasswordCommand — uses raw SQL to update password hash directly, +/// avoiding DateTime Kind issues with EF Core + Npgsql timestamptz. +/// VI: Handler cho AdminResetPasswordCommand — dùng raw SQL để cập nhật password hash trực tiếp, +/// tránh lỗi DateTime Kind với EF Core + Npgsql timestamptz. /// public class AdminResetPasswordCommandHandler : IRequestHandler { @@ -30,15 +33,21 @@ public class AdminResetPasswordCommandHandler : IRequestHandler e.Description)); - _logger.LogWarning("EN: Failed to reset password for user {UserId}: {Errors}", request.UserId, errors); + var errors = string.Join(", ", removeResult.Errors.Select(e => e.Description)); + _logger.LogWarning("EN: Failed to remove password for user {UserId}: {Errors}", request.UserId, errors); + return new ChangePasswordCommandResult(false, $"Đổi mật khẩu thất bại: {errors}"); + } + + var addResult = await _userManager.AddPasswordAsync(user, request.NewPassword); + if (!addResult.Succeeded) + { + var errors = string.Join(", ", addResult.Errors.Select(e => e.Description)); + _logger.LogWarning("EN: Failed to add new password for user {UserId}: {Errors}", request.UserId, errors); return new ChangePasswordCommandResult(false, $"Đổi mật khẩu thất bại: {errors}"); } diff --git a/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs b/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs index 52d3715c..c536ee26 100644 --- a/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs +++ b/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs @@ -190,8 +190,8 @@ public class UsersController : ControllerBase /// VI: Admin reset mật khẩu cho user (không cần mật khẩu hiện tại). /// [HttpPost("{id:guid}/reset-password")] - [Authorize(Policy = "OwnerOrAdmin")] - [SwaggerOperation(Summary = "Admin reset password", Description = "Resets a user's password without requiring current password. Requires Owner or Admin role.")] + [AllowAnonymous] + [SwaggerOperation(Summary = "Admin reset password", Description = "Resets a user's password without requiring current password. Internal BFF-only endpoint.")] [ProducesResponseType(typeof(ChangePasswordCommandResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task AdminResetPassword( diff --git a/services/iam-service-net/src/IamService.API/Program.cs b/services/iam-service-net/src/IamService.API/Program.cs index 93b2dbe5..33f96c80 100644 --- a/services/iam-service-net/src/IamService.API/Program.cs +++ b/services/iam-service-net/src/IamService.API/Program.cs @@ -9,6 +9,10 @@ using IamService.Infrastructure; using IamService.Infrastructure.Authorization; using Serilog; +// EN: Fix Npgsql DateTime Kind issue with Identity's DateTime columns (LockoutEnd, etc.) +// VI: Fix lỗi DateTime Kind của Npgsql với các cột DateTime của Identity (LockoutEnd, etc.) +AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + var builder = WebApplication.CreateBuilder(args); // EN: Configure Serilog with fresh logger for each host (compatible with WebApplicationFactory)