diff --git a/services/iam-service-net/src/IamService.API/Application/Common/ApiResponse.cs b/services/iam-service-net/src/IamService.API/Application/Common/ApiResponse.cs
new file mode 100644
index 00000000..4f4d6423
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Common/ApiResponse.cs
@@ -0,0 +1,127 @@
+namespace IamService.API.Application.Common;
+
+///
+/// EN: Standard API response wrapper.
+/// VI: Wrapper response API chuẩn.
+///
+/// Type of data in response / Kiểu dữ liệu trong response
+public class ApiResponse
+{
+ ///
+ /// EN: Indicates if the request was successful.
+ /// VI: Cho biết request có thành công không.
+ ///
+ /// true
+ public bool Success { get; set; }
+
+ ///
+ /// EN: Response data.
+ /// VI: Dữ liệu response.
+ ///
+ public T? Data { get; set; }
+
+ ///
+ /// EN: Error information if request failed.
+ /// VI: Thông tin lỗi nếu request thất bại.
+ ///
+ public ApiError? Error { get; set; }
+
+ ///
+ /// EN: Pagination information for list responses.
+ /// VI: Thông tin phân trang cho list responses.
+ ///
+ public PaginationInfo? Pagination { get; set; }
+
+ ///
+ /// EN: Create a successful response.
+ /// VI: Tạo response thành công.
+ ///
+ public static ApiResponse Ok(T data) => new()
+ {
+ Success = true,
+ Data = data
+ };
+
+ ///
+ /// EN: Create a successful paginated response.
+ /// VI: Tạo response thành công có phân trang.
+ ///
+ public static ApiResponse Ok(T data, PaginationInfo pagination) => new()
+ {
+ Success = true,
+ Data = data,
+ Pagination = pagination
+ };
+
+ ///
+ /// EN: Create a failed response.
+ /// VI: Tạo response thất bại.
+ ///
+ public static ApiResponse Fail(string code, string message) => new()
+ {
+ Success = false,
+ Error = new ApiError { Code = code, Message = message }
+ };
+}
+
+///
+/// EN: API error details.
+/// VI: Chi tiết lỗi API.
+///
+public class ApiError
+{
+ ///
+ /// EN: Error code for programmatic handling.
+ /// VI: Mã lỗi để xử lý lập trình.
+ ///
+ /// VALIDATION_ERROR
+ public string Code { get; set; } = string.Empty;
+
+ ///
+ /// EN: Human-readable error message.
+ /// VI: Thông báo lỗi dễ đọc.
+ ///
+ /// The email field is required.
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// EN: Additional error details.
+ /// VI: Chi tiết lỗi bổ sung.
+ ///
+ public IDictionary? Details { get; set; }
+}
+
+///
+/// EN: Pagination information.
+/// VI: Thông tin phân trang.
+///
+public class PaginationInfo
+{
+ ///
+ /// EN: Current page number (1-based).
+ /// VI: Số trang hiện tại (bắt đầu từ 1).
+ ///
+ /// 1
+ public int PageNumber { get; set; }
+
+ ///
+ /// EN: Number of items per page.
+ /// VI: Số items mỗi trang.
+ ///
+ /// 10
+ public int PageSize { get; set; }
+
+ ///
+ /// EN: Total number of items.
+ /// VI: Tổng số items.
+ ///
+ /// 100
+ public int TotalCount { get; set; }
+
+ ///
+ /// EN: Total number of pages.
+ /// VI: Tổng số trang.
+ ///
+ /// 10
+ public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
+}
diff --git a/services/iam-service-net/src/IamService.API/Application/Common/UserDto.cs b/services/iam-service-net/src/IamService.API/Application/Common/UserDto.cs
new file mode 100644
index 00000000..0abe8e0e
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/Application/Common/UserDto.cs
@@ -0,0 +1,106 @@
+namespace IamService.API.Application.Common;
+
+///
+/// EN: User data transfer object for API responses.
+/// VI: Data transfer object của user cho API responses.
+///
+public record UserDto
+{
+ ///
+ /// EN: User's unique identifier.
+ /// VI: Mã định danh duy nhất của user.
+ ///
+ /// 550e8400-e29b-41d4-a716-446655440000
+ public Guid Id { get; init; }
+
+ ///
+ /// EN: User's email address.
+ /// VI: Địa chỉ email của user.
+ ///
+ /// user@example.com
+ public string Email { get; init; } = string.Empty;
+
+ ///
+ /// EN: User's first name.
+ /// VI: Tên của user.
+ ///
+ /// John
+ public string FirstName { get; init; } = string.Empty;
+
+ ///
+ /// EN: User's last name.
+ /// VI: Họ của user.
+ ///
+ /// Doe
+ public string LastName { get; init; } = string.Empty;
+
+ ///
+ /// EN: User's full name.
+ /// VI: Họ tên đầy đủ của user.
+ ///
+ /// John Doe
+ public string FullName { get; init; } = string.Empty;
+
+ ///
+ /// EN: User's roles.
+ /// VI: Các role của user.
+ ///
+ /// ["admin", "user"]
+ public IEnumerable Roles { get; init; } = [];
+
+ ///
+ /// EN: User account status.
+ /// VI: Trạng thái tài khoản user.
+ ///
+ /// Active
+ public string Status { get; init; } = string.Empty;
+
+ ///
+ /// EN: When the user was created.
+ /// VI: Thời điểm user được tạo.
+ ///
+ /// 2026-01-12T06:30:00Z
+ public DateTime CreatedAt { get; init; }
+
+ ///
+ /// EN: When the user last logged in.
+ /// VI: Thời điểm user đăng nhập lần cuối.
+ ///
+ /// 2026-01-12T12:00:00Z
+ public DateTime? LastLoginAt { get; init; }
+}
+
+///
+/// EN: Current user info response.
+/// VI: Response thông tin user hiện tại.
+///
+public record CurrentUserDto
+{
+ ///
+ /// EN: User's unique identifier.
+ /// VI: Mã định danh duy nhất của user.
+ ///
+ /// 550e8400-e29b-41d4-a716-446655440000
+ public string Id { get; init; } = string.Empty;
+
+ ///
+ /// EN: User's email address.
+ /// VI: Địa chỉ email của user.
+ ///
+ /// user@example.com
+ public string? Email { get; init; }
+
+ ///
+ /// EN: User's display name.
+ /// VI: Tên hiển thị của user.
+ ///
+ /// John Doe
+ public string? Name { get; init; }
+
+ ///
+ /// EN: User's roles.
+ /// VI: Các role của user.
+ ///
+ /// ["admin", "user"]
+ public IEnumerable Roles { get; init; } = [];
+}
diff --git a/services/iam-service-net/src/IamService.API/Controllers/AuthController.cs b/services/iam-service-net/src/IamService.API/Controllers/AuthController.cs
index a6055ef5..be975634 100644
--- a/services/iam-service-net/src/IamService.API/Controllers/AuthController.cs
+++ b/services/iam-service-net/src/IamService.API/Controllers/AuthController.cs
@@ -7,7 +7,9 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
+using Swashbuckle.AspNetCore.Annotations;
using IamService.API.Application.Commands.Auth;
+using IamService.API.Application.Common;
using IamService.Domain.AggregatesModel.UserAggregate;
using static OpenIddict.Abstractions.OpenIddictConstants;
@@ -20,6 +22,7 @@ namespace IamService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/auth")]
+[SwaggerTag("Authentication endpoints - OAuth2/OIDC")]
public class AuthController : ControllerBase
{
private readonly IMediator _mediator;
@@ -43,17 +46,29 @@ public class AuthController : ControllerBase
/// EN: Register a new user.
/// VI: Đăng ký user mới.
///
+ /// User registration data
+ /// Cancellation token
+ /// Registered user information
[HttpPost("register")]
- [ProducesResponseType(typeof(RegisterUserCommandResult), StatusCodes.Status201Created)]
+ [SwaggerOperation(
+ Summary = "Register a new user",
+ Description = "Creates a new user account with email and password.",
+ OperationId = "RegisterUser")]
+ [SwaggerResponse(StatusCodes.Status201Created, "User successfully registered", typeof(ApiResponse))]
+ [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid registration data")]
+ [SwaggerResponse(StatusCodes.Status409Conflict, "User with this email already exists")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task Register(
- [FromBody] RegisterUserCommand command,
+ [FromBody, SwaggerRequestBody("User registration details", Required = true)] RegisterUserCommand command,
CancellationToken cancellationToken)
{
var result = await _mediator.Send(command, cancellationToken);
- return CreatedAtAction(nameof(Register), new { id = result.UserId }, result);
+ return CreatedAtAction(nameof(Register), new { id = result.UserId }, ApiResponse.Ok(result));
}
+
///
/// EN: OAuth2 Token endpoint (handled by OpenIddict).
/// VI: OAuth2 Token endpoint (được xử lý bởi OpenIddict).
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 2f86e52a..e16f6917 100644
--- a/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs
+++ b/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs
@@ -3,6 +3,8 @@ using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Validation.AspNetCore;
+using Swashbuckle.AspNetCore.Annotations;
+using IamService.API.Application.Common;
using IamService.API.Application.Queries.Users;
namespace IamService.API.Controllers;
@@ -15,6 +17,7 @@ namespace IamService.API.Controllers;
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
+[SwaggerTag("User management endpoints - requires authentication")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
@@ -32,26 +35,46 @@ public class UsersController : ControllerBase
/// EN: Get all users with pagination.
/// VI: Lấy tất cả users với phân trang.
///
+ /// Page number (1-based)
+ /// Number of items per page
+ /// Cancellation token
+ /// Paginated list of users
[HttpGet]
- [ProducesResponseType(typeof(GetUsersQueryResult), StatusCodes.Status200OK)]
+ [SwaggerOperation(
+ Summary = "Get all users",
+ Description = "Retrieves a paginated list of all users. Requires authentication.",
+ OperationId = "GetUsers")]
+ [SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved users", typeof(ApiResponse>))]
+ [SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task GetUsers(
- [FromQuery] int pageNumber = 1,
- [FromQuery] int pageSize = 10,
+ [FromQuery, SwaggerParameter("Page number (1-based)", Required = false)] int pageNumber = 1,
+ [FromQuery, SwaggerParameter("Number of items per page", Required = false)] int pageSize = 10,
CancellationToken cancellationToken = default)
{
var query = new GetUsersQuery(pageNumber, pageSize);
var result = await _mediator.Send(query, cancellationToken);
- return Ok(new
+ return Ok(new ApiResponse>
{
- success = true,
- data = result.Users,
- pagination = new
+ Success = true,
+ Data = result.Users.Select(u => new UserDto
{
- pageNumber = result.PageNumber,
- pageSize = result.PageSize,
- totalCount = result.TotalCount,
- totalPages = (int)Math.Ceiling(result.TotalCount / (double)result.PageSize)
+ Id = u.Id,
+ Email = u.Email ?? string.Empty,
+ FirstName = u.FirstName,
+ LastName = u.LastName,
+ FullName = u.FullName,
+ Status = u.Status,
+ CreatedAt = u.CreatedAt,
+ LastLoginAt = u.LastLoginAt
+ }),
+ Pagination = new PaginationInfo
+ {
+ PageNumber = result.PageNumber,
+ PageSize = result.PageSize,
+ TotalCount = result.TotalCount
}
});
}
@@ -60,8 +83,16 @@ public class UsersController : ControllerBase
/// EN: Get current user info.
/// VI: Lấy thông tin user hiện tại.
///
+ /// Current user information
[HttpGet("me")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [SwaggerOperation(
+ Summary = "Get current user",
+ Description = "Retrieves information about the currently authenticated user.",
+ OperationId = "GetCurrentUser")]
+ [SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved current user", typeof(ApiResponse))]
+ [SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public IActionResult GetCurrentUser()
{
var userId = User.FindFirst("sub")?.Value;
@@ -69,16 +100,13 @@ public class UsersController : ControllerBase
var name = User.FindFirst("name")?.Value;
var roles = User.FindAll("role").Select(c => c.Value);
- return Ok(new
+ return Ok(ApiResponse.Ok(new CurrentUserDto
{
- success = true,
- data = new
- {
- id = userId,
- email,
- name,
- roles
- }
- });
+ Id = userId ?? string.Empty,
+ Email = email,
+ Name = name,
+ Roles = roles
+ }));
}
}
+
diff --git a/services/iam-service-net/src/IamService.API/IamService.API.csproj b/services/iam-service-net/src/IamService.API/IamService.API.csproj
index d122c115..e49de080 100644
--- a/services/iam-service-net/src/IamService.API/IamService.API.csproj
+++ b/services/iam-service-net/src/IamService.API/IamService.API.csproj
@@ -5,6 +5,9 @@
IamService.API
Web API layer with CQRS pattern
iamservice-api
+
+ true
+ $(NoWarn);1591
@@ -17,6 +20,7 @@
+
diff --git a/services/iam-service-net/src/IamService.API/Program.cs b/services/iam-service-net/src/IamService.API/Program.cs
index 4777d9c4..bbc3adf3 100644
--- a/services/iam-service-net/src/IamService.API/Program.cs
+++ b/services/iam-service-net/src/IamService.API/Program.cs
@@ -73,13 +73,44 @@ try
{
Title = "IAM Service API",
Version = "v1",
- Description = "Identity and Access Management Service - OAuth2/OIDC API"
+ Description = """
+ Identity and Access Management Service - OAuth2/OIDC API
+
+ ## Authentication
+ This API uses OAuth2 with Password Grant and JWT Bearer tokens.
+
+ ## Endpoints
+ - **/api/v1/auth/register** - Register a new user
+ - **/connect/token** - OAuth2 token endpoint
+ - **/api/v1/users** - User management (requires authentication)
+ """,
+ Contact = new()
+ {
+ Name = "GoodGo Team",
+ Email = "support@goodgo.com",
+ Url = new Uri("https://github.com/goodgo")
+ },
+ License = new()
+ {
+ Name = "MIT License",
+ Url = new Uri("https://opensource.org/licenses/MIT")
+ }
});
+ // EN: Include XML comments for better documentation
+ // VI: Include XML comments để documentation tốt hơn
+ var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename);
+ if (File.Exists(xmlPath))
+ {
+ options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
+ }
+
// EN: Add OAuth2 security definition / VI: Thêm OAuth2 security definition
options.AddSecurityDefinition("oauth2", new()
{
Type = Microsoft.OpenApi.Models.SecuritySchemeType.OAuth2,
+ Description = "OAuth2 Password Grant flow. Use email as username.",
Flows = new()
{
Password = new()
@@ -87,23 +118,39 @@ try
TokenUrl = new Uri("/connect/token", UriKind.Relative),
Scopes = new Dictionary
{
- ["openid"] = "OpenID",
- ["profile"] = "Profile",
- ["email"] = "Email",
- ["roles"] = "Roles",
- ["api"] = "API access"
+ ["openid"] = "OpenID - Required for authentication",
+ ["profile"] = "Profile - Access to user profile information",
+ ["email"] = "Email - Access to user email",
+ ["roles"] = "Roles - Access to user roles",
+ ["api"] = "API - Full API access"
}
}
}
});
+ // EN: Add JWT Bearer security definition / VI: Thêm JWT Bearer security definition
+ options.AddSecurityDefinition("Bearer", new()
+ {
+ Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http,
+ Scheme = "bearer",
+ BearerFormat = "JWT",
+ Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'"
+ });
+
options.AddSecurityRequirement(new()
{
{
new() { Reference = new() { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "oauth2" } },
["api"]
+ },
+ {
+ new() { Reference = new() { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } },
+ Array.Empty()
}
});
+
+ // EN: Enable annotations / VI: Bật annotations
+ options.EnableAnnotations();
});
// EN: Add health checks / VI: Thêm health checks