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:
@@ -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);
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user