feat: Add new WebClientBase Blazor application and MktZaloService API, and update WhatsAppService and MktXService infrastructure.

This commit is contained in:
Ho Ngoc Hai
2026-01-19 01:19:00 +07:00
parent 2d731dbdb6
commit d285f1f9eb
49 changed files with 2646 additions and 4 deletions

View File

@@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Mvc;
using WebClientBase.Shared;
using WebClientBase.Shared.DTOs;
namespace WebClientBase.Server.Controllers;
/// <summary>
/// EN: Authentication API controller.
/// VI: Auth API controller.
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
/// <summary>
/// EN: Register a new user.
/// VI: Đăng ký user mới.
/// </summary>
/// <param name="request">Registration data with validation</param>
[HttpPost("register")]
[ProducesResponseType(typeof(ApiResponse<UserProfileDto>), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<ApiResponse<UserProfileDto>> 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<UserProfileDto>.Ok(profile));
}
/// <summary>
/// EN: Login with email and password.
/// VI: Đăng nhập với email và mật khẩu.
/// </summary>
[HttpPost("login")]
[ProducesResponseType(typeof(ApiResponse<UserProfileDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)]
public ActionResult<ApiResponse<UserProfileDto>> 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<UserProfileDto>.Ok(profile));
}
/// <summary>
/// EN: Get user profile by ID.
/// VI: Lấy user profile theo ID.
/// </summary>
[HttpGet("profile/{id:guid}")]
[ProducesResponseType(typeof(ApiResponse<UserProfileDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
public ActionResult<ApiResponse<UserProfileDto>> 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<UserProfileDto>.Ok(profile));
}
}

View File

@@ -0,0 +1,95 @@
using Microsoft.AspNetCore.Mvc;
using WebClientBase.Shared;
using WebClientBase.Shared.DTOs;
namespace WebClientBase.Server.Controllers;
/// <summary>
/// EN: Products API controller with automatic ModelState validation.
/// VI: Products API controller với validation ModelState tự động.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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<ProductDto> _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 }
};
/// <summary>
/// EN: Get all products.
/// VI: Lấy tất cả sản phẩm.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<List<ProductDto>>), StatusCodes.Status200OK)]
public ActionResult<ApiResponse<List<ProductDto>>> GetAll()
{
return Ok(ApiResponse<List<ProductDto>>.Ok(_products));
}
/// <summary>
/// EN: Create a new product.
/// VI: Tạo sản phẩm mới.
/// </summary>
/// <param name="request">Product data with validation</param>
/// <remarks>
/// 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);
/// </remarks>
[HttpPost]
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<ApiResponse<ProductDto>> 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<ProductDto>.Ok(product)
);
}
/// <summary>
/// EN: Update an existing product.
/// VI: Cập nhật sản phẩm đã có.
/// </summary>
[HttpPut]
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<ApiResponse<ProductDto>> 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<ProductDto>.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<ProductDto>.Ok(existing));
}
}

View File

@@ -0,0 +1,73 @@
/// <summary>
/// EN: ASP.NET Core Server with Blazor WebAssembly hosting.
/// VI: ASP.NET Core Server với Blazor WebAssembly hosting.
/// </summary>
var builder = WebApplication.CreateBuilder(args);
// ═══════════════════════════════════════════════════════════════════════════════
// EN: Add services to the container
// 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: Add OpenAPI/Swagger support
// VI: Thêm hỗ trợ OpenAPI/Swagger
builder.Services.AddOpenApi();
// EN: Add CORS for Blazor WebAssembly client
// VI: Thêm CORS cho Blazor WebAssembly client
builder.Services.AddCors(options =>
{
options.AddPolicy("BlazorClient", policy =>
{
policy.WithOrigins("https://localhost:5001", "http://localhost:5000")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// EN: Add health checks
// VI: Thêm health checks
builder.Services.AddHealthChecks();
var app = builder.Build();
// ═══════════════════════════════════════════════════════════════════════════════
// EN: Configure the HTTP request pipeline
// VI: Cấu hình HTTP request pipeline
// ═══════════════════════════════════════════════════════════════════════════════
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
// EN: Enable CORS
// VI: Kích hoạt CORS
app.UseCors("BlazorClient");
// 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: Serve Blazor WebAssembly static files (for hosted mode)
// VI: Phục vụ static files của Blazor WebAssembly (cho hosted mode)
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
// EN: Fallback to index.html for SPA routing
// VI: Fallback đến index.html cho SPA routing
app.MapFallbackToFile("index.html");
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5091",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7228;http://localhost:5091",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WebClientBase.Shared\WebClientBase.Shared.csproj" />
<ProjectReference Include="..\WebClientBase.Client\WebClientBase.Client.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@WebClientBase.Server_HostAddress = http://localhost:5091
GET {{WebClientBase.Server_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}