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.
This commit is contained in:
Ho Ngoc Hai
2026-01-12 13:52:12 +07:00
parent 07f96a8eb2
commit b9065fe858
6 changed files with 358 additions and 31 deletions

View File

@@ -0,0 +1,127 @@
namespace IamService.API.Application.Common;
/// <summary>
/// EN: Standard API response wrapper.
/// VI: Wrapper response API chuẩn.
/// </summary>
/// <typeparam name="T">Type of data in response / Kiểu dữ liệu trong response</typeparam>
public class ApiResponse<T>
{
/// <summary>
/// EN: Indicates if the request was successful.
/// VI: Cho biết request có thành công không.
/// </summary>
/// <example>true</example>
public bool Success { get; set; }
/// <summary>
/// EN: Response data.
/// VI: Dữ liệu response.
/// </summary>
public T? Data { get; set; }
/// <summary>
/// EN: Error information if request failed.
/// VI: Thông tin lỗi nếu request thất bại.
/// </summary>
public ApiError? Error { get; set; }
/// <summary>
/// EN: Pagination information for list responses.
/// VI: Thông tin phân trang cho list responses.
/// </summary>
public PaginationInfo? Pagination { get; set; }
/// <summary>
/// EN: Create a successful response.
/// VI: Tạo response thành công.
/// </summary>
public static ApiResponse<T> Ok(T data) => new()
{
Success = true,
Data = data
};
/// <summary>
/// EN: Create a successful paginated response.
/// VI: Tạo response thành công có phân trang.
/// </summary>
public static ApiResponse<T> Ok(T data, PaginationInfo pagination) => new()
{
Success = true,
Data = data,
Pagination = pagination
};
/// <summary>
/// EN: Create a failed response.
/// VI: Tạo response thất bại.
/// </summary>
public static ApiResponse<T> Fail(string code, string message) => new()
{
Success = false,
Error = new ApiError { Code = code, Message = message }
};
}
/// <summary>
/// EN: API error details.
/// VI: Chi tiết lỗi API.
/// </summary>
public class ApiError
{
/// <summary>
/// EN: Error code for programmatic handling.
/// VI: Mã lỗi để xử lý lập trình.
/// </summary>
/// <example>VALIDATION_ERROR</example>
public string Code { get; set; } = string.Empty;
/// <summary>
/// EN: Human-readable error message.
/// VI: Thông báo lỗi dễ đọc.
/// </summary>
/// <example>The email field is required.</example>
public string Message { get; set; } = string.Empty;
/// <summary>
/// EN: Additional error details.
/// VI: Chi tiết lỗi bổ sung.
/// </summary>
public IDictionary<string, string[]>? Details { get; set; }
}
/// <summary>
/// EN: Pagination information.
/// VI: Thông tin phân trang.
/// </summary>
public class PaginationInfo
{
/// <summary>
/// EN: Current page number (1-based).
/// VI: Số trang hiện tại (bắt đầu từ 1).
/// </summary>
/// <example>1</example>
public int PageNumber { get; set; }
/// <summary>
/// EN: Number of items per page.
/// VI: Số items mỗi trang.
/// </summary>
/// <example>10</example>
public int PageSize { get; set; }
/// <summary>
/// EN: Total number of items.
/// VI: Tổng số items.
/// </summary>
/// <example>100</example>
public int TotalCount { get; set; }
/// <summary>
/// EN: Total number of pages.
/// VI: Tổng số trang.
/// </summary>
/// <example>10</example>
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
}

View File

@@ -0,0 +1,106 @@
namespace IamService.API.Application.Common;
/// <summary>
/// EN: User data transfer object for API responses.
/// VI: Data transfer object của user cho API responses.
/// </summary>
public record UserDto
{
/// <summary>
/// EN: User's unique identifier.
/// VI: Mã định danh duy nhất của user.
/// </summary>
/// <example>550e8400-e29b-41d4-a716-446655440000</example>
public Guid Id { get; init; }
/// <summary>
/// EN: User's email address.
/// VI: Địa chỉ email của user.
/// </summary>
/// <example>user@example.com</example>
public string Email { get; init; } = string.Empty;
/// <summary>
/// EN: User's first name.
/// VI: Tên của user.
/// </summary>
/// <example>John</example>
public string FirstName { get; init; } = string.Empty;
/// <summary>
/// EN: User's last name.
/// VI: Họ của user.
/// </summary>
/// <example>Doe</example>
public string LastName { get; init; } = string.Empty;
/// <summary>
/// EN: User's full name.
/// VI: Họ tên đầy đủ của user.
/// </summary>
/// <example>John Doe</example>
public string FullName { get; init; } = string.Empty;
/// <summary>
/// EN: User's roles.
/// VI: Các role của user.
/// </summary>
/// <example>["admin", "user"]</example>
public IEnumerable<string> Roles { get; init; } = [];
/// <summary>
/// EN: User account status.
/// VI: Trạng thái tài khoản user.
/// </summary>
/// <example>Active</example>
public string Status { get; init; } = string.Empty;
/// <summary>
/// EN: When the user was created.
/// VI: Thời điểm user được tạo.
/// </summary>
/// <example>2026-01-12T06:30:00Z</example>
public DateTime CreatedAt { get; init; }
/// <summary>
/// EN: When the user last logged in.
/// VI: Thời điểm user đăng nhập lần cuối.
/// </summary>
/// <example>2026-01-12T12:00:00Z</example>
public DateTime? LastLoginAt { get; init; }
}
/// <summary>
/// EN: Current user info response.
/// VI: Response thông tin user hiện tại.
/// </summary>
public record CurrentUserDto
{
/// <summary>
/// EN: User's unique identifier.
/// VI: Mã định danh duy nhất của user.
/// </summary>
/// <example>550e8400-e29b-41d4-a716-446655440000</example>
public string Id { get; init; } = string.Empty;
/// <summary>
/// EN: User's email address.
/// VI: Địa chỉ email của user.
/// </summary>
/// <example>user@example.com</example>
public string? Email { get; init; }
/// <summary>
/// EN: User's display name.
/// VI: Tên hiển thị của user.
/// </summary>
/// <example>John Doe</example>
public string? Name { get; init; }
/// <summary>
/// EN: User's roles.
/// VI: Các role của user.
/// </summary>
/// <example>["admin", "user"]</example>
public IEnumerable<string> Roles { get; init; } = [];
}

View File

@@ -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.
/// </summary>
/// <param name="command">User registration data</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Registered user information</returns>
[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<RegisterUserCommandResult>))]
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid registration data")]
[SwaggerResponse(StatusCodes.Status409Conflict, "User with this email already exists")]
[ProducesResponseType(typeof(ApiResponse<RegisterUserCommandResult>), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> 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<RegisterUserCommandResult>.Ok(result));
}
/// <summary>
/// EN: OAuth2 Token endpoint (handled by OpenIddict).
/// VI: OAuth2 Token endpoint (được xử lý bởi OpenIddict).

View File

@@ -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.
/// </summary>
/// <param name="pageNumber">Page number (1-based)</param>
/// <param name="pageSize">Number of items per page</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Paginated list of users</returns>
[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<IEnumerable<UserDto>>))]
[SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
[ProducesResponseType(typeof(ApiResponse<IEnumerable<UserDto>>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> 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<IEnumerable<UserDto>>
{
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.
/// </summary>
/// <returns>Current user information</returns>
[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<CurrentUserDto>))]
[SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")]
[ProducesResponseType(typeof(ApiResponse<CurrentUserDto>), 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<CurrentUserDto>.Ok(new CurrentUserDto
{
success = true,
data = new
{
id = userId,
email,
name,
roles
}
});
Id = userId ?? string.Empty,
Email = email,
Name = name,
Roles = roles
}));
}
}

View File

@@ -5,6 +5,9 @@
<RootNamespace>IamService.API</RootNamespace>
<Description>Web API layer with CQRS pattern</Description>
<UserSecretsId>iamservice-api</UserSecretsId>
<!-- EN: Enable XML documentation for Swagger / VI: Bật XML documentation cho Swagger -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -17,6 +20,7 @@
<!-- EN: Swagger/OpenAPI / VI: Swagger/OpenAPI -->
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.2.0" />
<!-- EN: API Versioning / VI: API Versioning -->
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />

View File

@@ -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<string, string>
{
["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<string>()
}
});
// EN: Enable annotations / VI: Bật annotations
options.EnableAnnotations();
});
// EN: Add health checks / VI: Thêm health checks