diff --git a/apps/web-client-base-net/README.md b/apps/web-client-base-net/README.md index b75aaebd..e66babaa 100644 --- a/apps/web-client-base-net/README.md +++ b/apps/web-client-base-net/README.md @@ -1,22 +1,45 @@ # WebClientBase - Blazor Web App .NET 10 -Base frontend web application cho GoodGo Platform được xây dựng với Blazor WebAssembly Hosted. +Base frontend web application cho GoodGo Platform được xây dựng với Blazor WebAssembly + BFF Pattern. -## Features / Tính năng +## Architecture / Kiến trúc -- **Blazor WebAssembly Hosted** - Client chạy trong browser, Server host API -- **Shared Library Pattern** - DTOs và Validation được chia sẻ giữa Client và Server -- **Data Annotations Validation** - Viết 1 lần, dùng chung cả 2 đầu -- **[ApiController] Auto-Validation** - Server tự động validate, không cần `if (!ModelState.IsValid)` -- **Dark/Light Mode** - CSS variables với theme toggle -- **Glassmorphism UI** - Modern design với backdrop blur và shadows +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Browser │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Blazor WebAssembly Client │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────┬────────────────────────────────────┘ + │ /api/* + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ BFF (Backend for Frontend) │ +│ WebClientBase.Server + YARP │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Routes: /api/iam/** → iam-service │ │ +│ │ /api/merchants/** → merchant-service │ │ +│ │ /api/catalog/** → catalog-service │ │ +│ │ /api/orders/** → order-service │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────┬────────────────────────────────────┘ + │ Internal Network + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Microservices │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ IAM │ │ Merchant │ │ Catalog │ │ Order │ │ +│ │ :5101 │ │ :5102 │ │ :5103 │ │ :5104 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` ## Tech Stack | Layer | Technology | |-------|------------| | Client | Blazor WebAssembly (.NET 10) | -| Server | ASP.NET Core Web API (.NET 10) | +| BFF | ASP.NET Core + YARP Reverse Proxy | | Shared | Class Library với Data Annotations | | Styling | CSS Variables, Dark Mode | @@ -28,10 +51,11 @@ web-client-base-net/ ├── src/ │ ├── WebClientBase.Client/ # Blazor WebAssembly │ │ ├── Layout/ # MainLayout, NavMenu -│ │ ├── Pages/ # Razor pages (Products, Auth) +│ │ ├── Pages/ # Razor pages │ │ └── wwwroot/css/ # Design System CSS -│ ├── WebClientBase.Server/ # ASP.NET Core Host -│ │ └── Controllers/ # API Controllers +│ ├── WebClientBase.Server/ # BFF with YARP Proxy +│ │ ├── Program.cs # YARP configuration +│ │ └── yarp.json # Routes configuration │ └── WebClientBase.Shared/ # Shared Library │ └── DTOs/ # Data Transfer Objects └── Dockerfile @@ -46,71 +70,27 @@ cd apps/web-client-base-net # Restore packages dotnet restore -# Run Server (hosts both API and Blazor client) +# Run BFF Server (hosts Blazor client + YARP proxy) dotnet run --project src/WebClientBase.Server # Open browser -# https://localhost:5001 hoặc http://localhost:5000 +# http://localhost:5091 ``` -## Shared Validation Pattern / Mẫu Validation Chia Sẻ +## BFF Routes Configuration -### 1. Define DTO with Validation (một lần) -```csharp -// WebClientBase.Shared/DTOs/ProductDto.cs -public class ProductDto -{ - [Required(ErrorMessage = "Tên là bắt buộc / Name is required")] - [StringLength(100, MinimumLength = 3)] - public string Name { get; set; } = string.Empty; +Routes được cấu hình trong `yarp.json`: - [Range(0.01, 1_000_000)] - public decimal Price { get; set; } -} -``` +| Client Route | Proxied To | Description | +|--------------|------------|-------------| +| `/api/iam/**` | `http://localhost:5101/` | Identity & Access Management | +| `/api/merchants/**` | `http://localhost:5102/` | Merchant Service | +| `/api/catalog/**` | `http://localhost:5103/` | Catalog Service | +| `/api/orders/**` | `http://localhost:5104/` | Order Service | -### 2. Server (Auto-validation với [ApiController]) -```csharp -[ApiController] -[Route("api/[controller]")] -public class ProductsController : ControllerBase -{ - [HttpPost] - public IActionResult Create(ProductDto request) - { - // Không cần if (!ModelState.IsValid) - [ApiController] đã làm hộ - return Ok(ApiResponse.Ok(request)); - } -} -``` - -### 3. Client (DataAnnotationsValidator) -```razor - - - - - - - - - -``` - -## API Endpoints - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/api/products` | Get all products | -| POST | `/api/products` | Create product | -| PUT | `/api/products` | Update product | -| POST | `/api/auth/register` | Register user | -| POST | `/api/auth/login` | Login | -| GET | `/api/auth/profile/{id}` | Get profile | -| GET | `/health` | Health check | +> **Note:** Addresses có thể override qua environment variables trong production. ## Related Skills -- [React Enterprise Architect](../../.agent/skills/react-enterprise-architect/SKILL.md) -- [Tailwind Design System](../../.agent/skills/tailwind-design-system/SKILL.md) +- [API Aggregation](../../.agent/skills/api-aggregation/SKILL.md) - BFF Pattern - [Project Rules](../../.agent/skills/project-rules/SKILL.md) diff --git a/apps/web-client-base-net/src/WebClientBase.Server/Controllers/AuthController.cs b/apps/web-client-base-net/src/WebClientBase.Server/Controllers/AuthController.cs deleted file mode 100644 index 5e49b515..00000000 --- a/apps/web-client-base-net/src/WebClientBase.Server/Controllers/AuthController.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using WebClientBase.Shared; -using WebClientBase.Shared.DTOs; - -namespace WebClientBase.Server.Controllers; - -/// -/// EN: Authentication API controller. -/// VI: Auth API controller. -/// -[ApiController] -[Route("api/[controller]")] -public class AuthController : ControllerBase -{ - /// - /// EN: Register a new user. - /// VI: Đăng ký user mới. - /// - /// Registration data with validation - [HttpPost("register")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] - public ActionResult> Register([FromBody] RegisterDto request) - { - // EN: [ApiController] validates before this code runs - // VI: [ApiController] validate trước khi code này chạy - - // EN: Demo - create user profile - // VI: Demo - tạo user profile - var profile = new UserProfileDto - { - Id = Guid.NewGuid(), - Email = request.Email, - DisplayName = request.DisplayName, - CreatedAt = DateTime.UtcNow - }; - - return CreatedAtAction(nameof(GetProfile), new { id = profile.Id }, ApiResponse.Ok(profile)); - } - - /// - /// EN: Login with email and password. - /// VI: Đăng nhập với email và mật khẩu. - /// - [HttpPost("login")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] - public ActionResult> Login([FromBody] LoginDto request) - { - // EN: Demo - always succeed with mock profile - // VI: Demo - luôn thành công với profile giả - var profile = new UserProfileDto - { - Id = Guid.NewGuid(), - Email = request.Email, - DisplayName = "Demo User", - CreatedAt = DateTime.UtcNow.AddDays(-30) - }; - - return Ok(ApiResponse.Ok(profile)); - } - - /// - /// EN: Get user profile by ID. - /// VI: Lấy user profile theo ID. - /// - [HttpGet("profile/{id:guid}")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public ActionResult> GetProfile(Guid id) - { - // EN: Demo - return mock profile - // VI: Demo - trả về profile giả - var profile = new UserProfileDto - { - Id = id, - Email = "demo@example.com", - DisplayName = "Demo User", - AvatarUrl = "https://example.com/avatar.jpg", - CreatedAt = DateTime.UtcNow.AddDays(-30) - }; - - return Ok(ApiResponse.Ok(profile)); - } -} diff --git a/apps/web-client-base-net/src/WebClientBase.Server/Controllers/ProductsController.cs b/apps/web-client-base-net/src/WebClientBase.Server/Controllers/ProductsController.cs deleted file mode 100644 index 66b9df06..00000000 --- a/apps/web-client-base-net/src/WebClientBase.Server/Controllers/ProductsController.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using WebClientBase.Shared; -using WebClientBase.Shared.DTOs; - -namespace WebClientBase.Server.Controllers; - -/// -/// EN: Products API controller with automatic ModelState validation. -/// VI: Products API controller với validation ModelState tự động. -/// -/// -/// EN: The [ApiController] attribute automatically validates ModelState before action executes. -/// VI: Attribute [ApiController] tự động validate ModelState trước khi action được thực thi. -/// -[ApiController] -[Route("api/[controller]")] -public class ProductsController : ControllerBase -{ - // EN: In-memory storage for demo purposes - // VI: Lưu trữ trong bộ nhớ cho mục đích demo - private static readonly List _products = new() - { - new ProductDto { Name = "MacBook Pro", Description = "Laptop cao cấp", Price = 2499.99m, Quantity = 10 }, - new ProductDto { Name = "iPhone 15 Pro", Description = "Điện thoại thông minh", Price = 1199.99m, Quantity = 50 } - }; - - /// - /// EN: Get all products. - /// VI: Lấy tất cả sản phẩm. - /// - [HttpGet] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public ActionResult>> GetAll() - { - return Ok(ApiResponse>.Ok(_products)); - } - - /// - /// EN: Create a new product. - /// VI: Tạo sản phẩm mới. - /// - /// Product data with validation - /// - /// EN: [ApiController] automatically returns 400 if ModelState is invalid. - /// VI: [ApiController] tự động trả về 400 nếu ModelState không hợp lệ. - /// No need for: if (!ModelState.IsValid) return BadRequest(ModelState); - /// - [HttpPost] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] - public ActionResult> Create([FromBody] CreateProductDto request) - { - // EN: Validation is automatically done by [ApiController] before reaching here - // VI: Validation được tự động thực hiện bởi [ApiController] trước khi đến đây - - var product = new ProductDto - { - Name = request.Name, - Description = request.Description, - Price = request.Price, - Quantity = request.Quantity - }; - - _products.Add(product); - - return CreatedAtAction( - nameof(GetAll), - ApiResponse.Ok(product) - ); - } - - /// - /// EN: Update an existing product. - /// VI: Cập nhật sản phẩm đã có. - /// - [HttpPut] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] - public ActionResult> Update([FromBody] UpdateProductDto request) - { - // EN: Find and update (simplified for demo) - // VI: Tìm và cập nhật (đơn giản hóa cho demo) - var existing = _products.FirstOrDefault(p => p.Name == request.Name); - if (existing == null) - { - return NotFound(ApiResponse.Fail("Sản phẩm không tồn tại / Product not found")); - } - - existing.Description = request.Description; - existing.Price = request.Price; - existing.Quantity = request.Quantity; - - return Ok(ApiResponse.Ok(existing)); - } -} diff --git a/apps/web-client-base-net/src/WebClientBase.Server/Program.cs b/apps/web-client-base-net/src/WebClientBase.Server/Program.cs index 7c5c045a..c128fdcb 100644 --- a/apps/web-client-base-net/src/WebClientBase.Server/Program.cs +++ b/apps/web-client-base-net/src/WebClientBase.Server/Program.cs @@ -1,6 +1,6 @@ /// -/// EN: ASP.NET Core Server with Blazor WebAssembly hosting. -/// VI: ASP.NET Core Server với Blazor WebAssembly hosting. +/// EN: ASP.NET Core BFF (Backend for Frontend) with YARP Reverse Proxy. +/// VI: ASP.NET Core BFF (Backend for Frontend) với YARP Reverse Proxy. /// var builder = WebApplication.CreateBuilder(args); @@ -10,9 +10,14 @@ var builder = WebApplication.CreateBuilder(args); // VI: Thêm các services vào container // ═══════════════════════════════════════════════════════════════════════════════ -// EN: Add controllers with automatic ModelState validation via [ApiController] -// VI: Thêm controllers với validation ModelState tự động qua [ApiController] -builder.Services.AddControllers(); +// EN: Load YARP configuration from yarp.json +// VI: Load cấu hình YARP từ yarp.json +builder.Configuration.AddJsonFile("yarp.json", optional: false, reloadOnChange: true); + +// EN: Add YARP Reverse Proxy +// VI: Thêm YARP Reverse Proxy +builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); // EN: Add OpenAPI/Swagger support // VI: Thêm hỗ trợ OpenAPI/Swagger @@ -24,7 +29,7 @@ builder.Services.AddCors(options => { options.AddPolicy("BlazorClient", policy => { - policy.WithOrigins("https://localhost:5001", "http://localhost:5000") + policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); @@ -55,16 +60,15 @@ app.UseCors("BlazorClient"); // EN: Serve static files with fingerprinting support (.NET 10+) // VI: Phục vụ static files với hỗ trợ fingerprinting (.NET 10+) -// IMPORTANT: MapStaticAssets handles fingerprinted file mappings automatically app.MapStaticAssets(); // EN: Map health check endpoint // VI: Map endpoint health check app.MapHealthChecks("/health"); -// EN: Map API controllers -// VI: Map các API controllers -app.MapControllers(); +// EN: Map YARP Reverse Proxy routes to microservices +// VI: Map các routes YARP Reverse Proxy đến microservices +app.MapReverseProxy(); // EN: Fallback to index.html for SPA routing // VI: Fallback đến index.html cho SPA routing diff --git a/apps/web-client-base-net/src/WebClientBase.Server/WebClientBase.Server.csproj b/apps/web-client-base-net/src/WebClientBase.Server/WebClientBase.Server.csproj index e5e20fcb..d3af3ce5 100644 --- a/apps/web-client-base-net/src/WebClientBase.Server/WebClientBase.Server.csproj +++ b/apps/web-client-base-net/src/WebClientBase.Server/WebClientBase.Server.csproj @@ -9,6 +9,7 @@ + diff --git a/apps/web-client-base-net/src/WebClientBase.Server/yarp.json b/apps/web-client-base-net/src/WebClientBase.Server/yarp.json new file mode 100644 index 00000000..d6f1d03e --- /dev/null +++ b/apps/web-client-base-net/src/WebClientBase.Server/yarp.json @@ -0,0 +1,80 @@ +{ + "ReverseProxy": { + "Routes": { + "iam-route": { + "ClusterId": "iam-cluster", + "Match": { + "Path": "/api/iam/{**catch-all}" + }, + "Transforms": [ + { + "PathRemovePrefix": "/api/iam" + } + ] + }, + "merchant-route": { + "ClusterId": "merchant-cluster", + "Match": { + "Path": "/api/merchants/{**catch-all}" + }, + "Transforms": [ + { + "PathRemovePrefix": "/api/merchants" + } + ] + }, + "catalog-route": { + "ClusterId": "catalog-cluster", + "Match": { + "Path": "/api/catalog/{**catch-all}" + }, + "Transforms": [ + { + "PathRemovePrefix": "/api/catalog" + } + ] + }, + "order-route": { + "ClusterId": "order-cluster", + "Match": { + "Path": "/api/orders/{**catch-all}" + }, + "Transforms": [ + { + "PathRemovePrefix": "/api/orders" + } + ] + } + }, + "Clusters": { + "iam-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5101" + } + } + }, + "merchant-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5102" + } + } + }, + "catalog-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5103" + } + } + }, + "order-cluster": { + "Destinations": { + "destination1": { + "Address": "http://localhost:5104" + } + } + } + } + } +} \ No newline at end of file