From b9065fe8581da6c5bc4a14b2cc8f1eadc9ead603 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Mon, 12 Jan 2026 13:52:12 +0700 Subject: [PATCH] feat(docs): Enhance API documentation and Swagger integration - Enabled XML documentation generation for improved API documentation. - Updated API descriptions and added detailed endpoint information for better clarity. - Introduced Swagger annotations for authentication and user management endpoints. - Enhanced response types and added pagination information in user-related responses. - Included contact and license information in the API metadata for better transparency. --- .../Application/Common/ApiResponse.cs | 127 ++++++++++++++++++ .../Application/Common/UserDto.cs | 106 +++++++++++++++ .../Controllers/AuthController.cs | 21 ++- .../Controllers/UsersController.cs | 72 +++++++--- .../src/IamService.API/IamService.API.csproj | 4 + .../src/IamService.API/Program.cs | 59 +++++++- 6 files changed, 358 insertions(+), 31 deletions(-) create mode 100644 services/iam-service-net/src/IamService.API/Application/Common/ApiResponse.cs create mode 100644 services/iam-service-net/src/IamService.API/Application/Common/UserDto.cs 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