feat: Add Access Request functionality to IAM Service
- Introduced new Access Request and Access Request Approver entities in the DbContext to support access management features. - Updated Dependency Injection to include the AccessRequestRepository, enhancing the service's capabilities for handling access requests. - Added example curl command for token retrieval using the test account, improving developer experience for testing authentication flows.
This commit is contained in:
16
note.md
16
note.md
@@ -1,3 +1,17 @@
|
||||
Test Account
|
||||
Tài khoản: hongochai10@icloud.com
|
||||
Mật Khẩu: Velik@2026
|
||||
Mật Khẩu: Velik@2026
|
||||
|
||||
|
||||
curl -s -X POST "http
|
||||
://localhost:5001/connect/token" \
|
||||
> -H "Content-Type: application/x-www-fo
|
||||
rm-urlencoded" \
|
||||
> -d "grant_type=password" \
|
||||
> -d "client_id=password-client" \
|
||||
> -d "client_secret=password-client-secret" \
|
||||
> -d "username=hongochai10@icloud.com" \
|
||||
|
||||
> -d "password=Velik@2026" \
|
||||
> -d "scope=openid profile email api offline_access" 2>
|
||||
&1 | jq .
|
||||
@@ -0,0 +1,159 @@
|
||||
using MediatR;
|
||||
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
using IamService.Domain.Exceptions;
|
||||
|
||||
namespace IamService.API.Application.Commands.AccessRequests;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for CreateAccessRequestCommand.
|
||||
/// VI: Handler cho CreateAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class CreateAccessRequestCommandHandler : IRequestHandler<CreateAccessRequestCommand, CreateAccessRequestCommandResult>
|
||||
{
|
||||
private readonly IAccessRequestRepository _repository;
|
||||
|
||||
public CreateAccessRequestCommandHandler(IAccessRequestRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<CreateAccessRequestCommandResult> Handle(
|
||||
CreateAccessRequestCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var priority = request.Priority.HasValue
|
||||
? AccessRequestPriority.FromId(request.Priority.Value)
|
||||
: AccessRequestPriority.Medium;
|
||||
|
||||
var accessRequest = AccessRequest.Create(
|
||||
request.RequesterId,
|
||||
request.ResourceType,
|
||||
request.ResourceId,
|
||||
request.RequestedPermission,
|
||||
request.Justification,
|
||||
priority);
|
||||
|
||||
// EN: Add approvers
|
||||
// VI: Thêm approvers
|
||||
foreach (var approverId in request.ApproverIds)
|
||||
{
|
||||
accessRequest.AddApprover(approverId);
|
||||
}
|
||||
|
||||
_repository.Add(accessRequest);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
return new CreateAccessRequestCommandResult(
|
||||
accessRequest.Id,
|
||||
accessRequest.Status.Name,
|
||||
accessRequest.CreatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for SubmitAccessRequestCommand.
|
||||
/// VI: Handler cho SubmitAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class SubmitAccessRequestCommandHandler : IRequestHandler<SubmitAccessRequestCommand, Unit>
|
||||
{
|
||||
private readonly IAccessRequestRepository _repository;
|
||||
|
||||
public SubmitAccessRequestCommandHandler(IAccessRequestRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(SubmitAccessRequestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accessRequest = await _repository.GetByIdWithApproversAsync(request.RequestId, cancellationToken);
|
||||
if (accessRequest == null)
|
||||
throw new DomainException($"Access request {request.RequestId} not found.");
|
||||
|
||||
accessRequest.Submit();
|
||||
_repository.Update(accessRequest);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for ApproveAccessRequestCommand.
|
||||
/// VI: Handler cho ApproveAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class ApproveAccessRequestCommandHandler : IRequestHandler<ApproveAccessRequestCommand, Unit>
|
||||
{
|
||||
private readonly IAccessRequestRepository _repository;
|
||||
|
||||
public ApproveAccessRequestCommandHandler(IAccessRequestRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(ApproveAccessRequestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accessRequest = await _repository.GetByIdWithApproversAsync(request.RequestId, cancellationToken);
|
||||
if (accessRequest == null)
|
||||
throw new DomainException($"Access request {request.RequestId} not found.");
|
||||
|
||||
accessRequest.Approve(request.ApproverId, request.Comments);
|
||||
_repository.Update(accessRequest);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for RejectAccessRequestCommand.
|
||||
/// VI: Handler cho RejectAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class RejectAccessRequestCommandHandler : IRequestHandler<RejectAccessRequestCommand, Unit>
|
||||
{
|
||||
private readonly IAccessRequestRepository _repository;
|
||||
|
||||
public RejectAccessRequestCommandHandler(IAccessRequestRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(RejectAccessRequestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accessRequest = await _repository.GetByIdWithApproversAsync(request.RequestId, cancellationToken);
|
||||
if (accessRequest == null)
|
||||
throw new DomainException($"Access request {request.RequestId} not found.");
|
||||
|
||||
accessRequest.Reject(request.ApproverId, request.Reason);
|
||||
_repository.Update(accessRequest);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for CancelAccessRequestCommand.
|
||||
/// VI: Handler cho CancelAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class CancelAccessRequestCommandHandler : IRequestHandler<CancelAccessRequestCommand, Unit>
|
||||
{
|
||||
private readonly IAccessRequestRepository _repository;
|
||||
|
||||
public CancelAccessRequestCommandHandler(IAccessRequestRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(CancelAccessRequestCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accessRequest = await _repository.GetByIdAsync(request.RequestId, cancellationToken);
|
||||
if (accessRequest == null)
|
||||
throw new DomainException($"Access request {request.RequestId} not found.");
|
||||
|
||||
accessRequest.Cancel();
|
||||
_repository.Update(accessRequest);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using MediatR;
|
||||
|
||||
namespace IamService.API.Application.Commands.AccessRequests;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to create a new access request.
|
||||
/// VI: Command để tạo yêu cầu truy cập mới.
|
||||
/// </summary>
|
||||
public record CreateAccessRequestCommand(
|
||||
Guid RequesterId,
|
||||
string ResourceType,
|
||||
Guid ResourceId,
|
||||
string RequestedPermission,
|
||||
string? Justification,
|
||||
int? Priority,
|
||||
List<Guid> ApproverIds) : IRequest<CreateAccessRequestCommandResult>;
|
||||
|
||||
public record CreateAccessRequestCommandResult(
|
||||
Guid Id,
|
||||
string Status,
|
||||
DateTime CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to submit access request for approval.
|
||||
/// VI: Command để gửi yêu cầu truy cập để phê duyệt.
|
||||
/// </summary>
|
||||
public record SubmitAccessRequestCommand(Guid RequestId) : IRequest<Unit>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to approve access request.
|
||||
/// VI: Command để phê duyệt yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public record ApproveAccessRequestCommand(
|
||||
Guid RequestId,
|
||||
Guid ApproverId,
|
||||
string? Comments) : IRequest<Unit>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to reject access request.
|
||||
/// VI: Command để từ chối yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public record RejectAccessRequestCommand(
|
||||
Guid RequestId,
|
||||
Guid ApproverId,
|
||||
string? Reason) : IRequest<Unit>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to cancel access request.
|
||||
/// VI: Command để hủy yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public record CancelAccessRequestCommand(Guid RequestId) : IRequest<Unit>;
|
||||
@@ -0,0 +1,52 @@
|
||||
using MediatR;
|
||||
|
||||
namespace IamService.API.Application.Queries.AccessRequests;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get access request by ID.
|
||||
/// VI: Query để lấy yêu cầu truy cập theo ID.
|
||||
/// </summary>
|
||||
public record GetAccessRequestByIdQuery(Guid Id) : IRequest<AccessRequestDto?>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get my access requests.
|
||||
/// VI: Query để lấy yêu cầu truy cập của tôi.
|
||||
/// </summary>
|
||||
public record GetMyAccessRequestsQuery(Guid RequesterId) : IRequest<IEnumerable<AccessRequestDto>>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get pending approvals for current user.
|
||||
/// VI: Query để lấy các yêu cầu đang chờ phê duyệt của user hiện tại.
|
||||
/// </summary>
|
||||
public record GetPendingApprovalsQuery(Guid ApproverId) : IRequest<IEnumerable<AccessRequestDto>>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access request DTO.
|
||||
/// VI: DTO cho yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public record AccessRequestDto(
|
||||
Guid Id,
|
||||
Guid RequesterId,
|
||||
string ResourceType,
|
||||
Guid ResourceId,
|
||||
string RequestedPermission,
|
||||
string? Justification,
|
||||
string Status,
|
||||
string Priority,
|
||||
DateTime CreatedAt,
|
||||
DateTime? SubmittedAt,
|
||||
DateTime? ResolvedAt,
|
||||
DateTime? ExpiresAt,
|
||||
IEnumerable<AccessRequestApproverDto> Approvers);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access request approver DTO.
|
||||
/// VI: DTO cho người phê duyệt yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public record AccessRequestApproverDto(
|
||||
Guid Id,
|
||||
Guid UserId,
|
||||
int Order,
|
||||
string Status,
|
||||
DateTime? RespondedAt,
|
||||
string? Comments);
|
||||
@@ -0,0 +1,127 @@
|
||||
using MediatR;
|
||||
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
|
||||
namespace IamService.API.Application.Queries.AccessRequests;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetAccessRequestByIdQuery.
|
||||
/// VI: Handler cho GetAccessRequestByIdQuery.
|
||||
/// </summary>
|
||||
public class GetAccessRequestByIdQueryHandler : IRequestHandler<GetAccessRequestByIdQuery, AccessRequestDto?>
|
||||
{
|
||||
private readonly IAccessRequestRepository _repository;
|
||||
|
||||
public GetAccessRequestByIdQueryHandler(IAccessRequestRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<AccessRequestDto?> Handle(GetAccessRequestByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accessRequest = await _repository.GetByIdWithApproversAsync(request.Id, cancellationToken);
|
||||
return accessRequest != null ? MapToDto(accessRequest) : null;
|
||||
}
|
||||
|
||||
private static AccessRequestDto MapToDto(AccessRequest request) => new(
|
||||
request.Id,
|
||||
request.RequesterId,
|
||||
request.ResourceType,
|
||||
request.ResourceId,
|
||||
request.RequestedPermission,
|
||||
request.Justification,
|
||||
request.Status.Name,
|
||||
request.Priority.Name,
|
||||
request.CreatedAt,
|
||||
request.SubmittedAt,
|
||||
request.ResolvedAt,
|
||||
request.ExpiresAt,
|
||||
request.Approvers.Select(a => new AccessRequestApproverDto(
|
||||
a.Id,
|
||||
a.UserId,
|
||||
a.Order,
|
||||
a.Status.Name,
|
||||
a.RespondedAt,
|
||||
a.Comments)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetMyAccessRequestsQuery.
|
||||
/// VI: Handler cho GetMyAccessRequestsQuery.
|
||||
/// </summary>
|
||||
public class GetMyAccessRequestsQueryHandler : IRequestHandler<GetMyAccessRequestsQuery, IEnumerable<AccessRequestDto>>
|
||||
{
|
||||
private readonly IAccessRequestRepository _repository;
|
||||
|
||||
public GetMyAccessRequestsQueryHandler(IAccessRequestRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AccessRequestDto>> Handle(GetMyAccessRequestsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var requests = await _repository.GetByRequesterIdAsync(request.RequesterId, cancellationToken);
|
||||
return requests.Select(MapToDto);
|
||||
}
|
||||
|
||||
private static AccessRequestDto MapToDto(AccessRequest request) => new(
|
||||
request.Id,
|
||||
request.RequesterId,
|
||||
request.ResourceType,
|
||||
request.ResourceId,
|
||||
request.RequestedPermission,
|
||||
request.Justification,
|
||||
request.Status.Name,
|
||||
request.Priority.Name,
|
||||
request.CreatedAt,
|
||||
request.SubmittedAt,
|
||||
request.ResolvedAt,
|
||||
request.ExpiresAt,
|
||||
request.Approvers.Select(a => new AccessRequestApproverDto(
|
||||
a.Id,
|
||||
a.UserId,
|
||||
a.Order,
|
||||
a.Status.Name,
|
||||
a.RespondedAt,
|
||||
a.Comments)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetPendingApprovalsQuery.
|
||||
/// VI: Handler cho GetPendingApprovalsQuery.
|
||||
/// </summary>
|
||||
public class GetPendingApprovalsQueryHandler : IRequestHandler<GetPendingApprovalsQuery, IEnumerable<AccessRequestDto>>
|
||||
{
|
||||
private readonly IAccessRequestRepository _repository;
|
||||
|
||||
public GetPendingApprovalsQueryHandler(IAccessRequestRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AccessRequestDto>> Handle(GetPendingApprovalsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var requests = await _repository.GetPendingByApproverIdAsync(request.ApproverId, cancellationToken);
|
||||
return requests.Select(MapToDto);
|
||||
}
|
||||
|
||||
private static AccessRequestDto MapToDto(AccessRequest request) => new(
|
||||
request.Id,
|
||||
request.RequesterId,
|
||||
request.ResourceType,
|
||||
request.ResourceId,
|
||||
request.RequestedPermission,
|
||||
request.Justification,
|
||||
request.Status.Name,
|
||||
request.Priority.Name,
|
||||
request.CreatedAt,
|
||||
request.SubmittedAt,
|
||||
request.ResolvedAt,
|
||||
request.ExpiresAt,
|
||||
request.Approvers.Select(a => new AccessRequestApproverDto(
|
||||
a.Id,
|
||||
a.UserId,
|
||||
a.Order,
|
||||
a.Status.Name,
|
||||
a.RespondedAt,
|
||||
a.Comments)));
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using FluentValidation;
|
||||
using IamService.API.Application.Commands.AccessRequests;
|
||||
|
||||
namespace IamService.API.Application.Validations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for CreateAccessRequestCommand.
|
||||
/// VI: Validator cho CreateAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class CreateAccessRequestCommandValidator : AbstractValidator<CreateAccessRequestCommand>
|
||||
{
|
||||
private static readonly string[] ValidResourceTypes = ["Organization", "Group", "Role", "Application", "Resource"];
|
||||
|
||||
public CreateAccessRequestCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.RequesterId)
|
||||
.NotEmpty().WithMessage("Requester ID is required");
|
||||
|
||||
RuleFor(x => x.ResourceType)
|
||||
.NotEmpty().WithMessage("Resource type is required")
|
||||
.MaximumLength(100).WithMessage("Resource type cannot exceed 100 characters")
|
||||
.Must(t => ValidResourceTypes.Contains(t))
|
||||
.WithMessage($"Resource type must be one of: {string.Join(", ", ValidResourceTypes)}");
|
||||
|
||||
RuleFor(x => x.ResourceId)
|
||||
.NotEmpty().WithMessage("Resource ID is required");
|
||||
|
||||
RuleFor(x => x.RequestedPermission)
|
||||
.NotEmpty().WithMessage("Requested permission is required")
|
||||
.MaximumLength(100).WithMessage("Requested permission cannot exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.Justification)
|
||||
.MaximumLength(2000).WithMessage("Justification cannot exceed 2000 characters")
|
||||
.When(x => x.Justification != null);
|
||||
|
||||
RuleFor(x => x.Priority)
|
||||
.Must(p => p == null || (p >= 1 && p <= 4))
|
||||
.WithMessage("Priority must be 1 (Low), 2 (Medium), 3 (High), or 4 (Critical)");
|
||||
|
||||
RuleFor(x => x.ApproverIds)
|
||||
.NotEmpty().WithMessage("At least one approver is required")
|
||||
.Must(ids => ids.All(id => id != Guid.Empty))
|
||||
.WithMessage("All approver IDs must be valid GUIDs");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for ApproveAccessRequestCommand.
|
||||
/// VI: Validator cho ApproveAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class ApproveAccessRequestCommandValidator : AbstractValidator<ApproveAccessRequestCommand>
|
||||
{
|
||||
public ApproveAccessRequestCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.RequestId)
|
||||
.NotEmpty().WithMessage("Request ID is required");
|
||||
|
||||
RuleFor(x => x.ApproverId)
|
||||
.NotEmpty().WithMessage("Approver ID is required");
|
||||
|
||||
RuleFor(x => x.Comments)
|
||||
.MaximumLength(1000).WithMessage("Comments cannot exceed 1000 characters")
|
||||
.When(x => x.Comments != null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for RejectAccessRequestCommand.
|
||||
/// VI: Validator cho RejectAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class RejectAccessRequestCommandValidator : AbstractValidator<RejectAccessRequestCommand>
|
||||
{
|
||||
public RejectAccessRequestCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.RequestId)
|
||||
.NotEmpty().WithMessage("Request ID is required");
|
||||
|
||||
RuleFor(x => x.ApproverId)
|
||||
.NotEmpty().WithMessage("Approver ID is required");
|
||||
|
||||
RuleFor(x => x.Reason)
|
||||
.MaximumLength(1000).WithMessage("Reason cannot exceed 1000 characters")
|
||||
.When(x => x.Reason != null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for SubmitAccessRequestCommand.
|
||||
/// VI: Validator cho SubmitAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class SubmitAccessRequestCommandValidator : AbstractValidator<SubmitAccessRequestCommand>
|
||||
{
|
||||
public SubmitAccessRequestCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.RequestId)
|
||||
.NotEmpty().WithMessage("Request ID is required");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validator for CancelAccessRequestCommand.
|
||||
/// VI: Validator cho CancelAccessRequestCommand.
|
||||
/// </summary>
|
||||
public class CancelAccessRequestCommandValidator : AbstractValidator<CancelAccessRequestCommand>
|
||||
{
|
||||
public CancelAccessRequestCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.RequestId)
|
||||
.NotEmpty().WithMessage("Request ID is required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using IamService.API.Application.Common;
|
||||
using IamService.API.Application.Commands.AccessRequests;
|
||||
using IamService.API.Application.Queries.AccessRequests;
|
||||
|
||||
namespace IamService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access request management controller.
|
||||
/// VI: Controller quản lý yêu cầu truy cập.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/access-requests")]
|
||||
[Authorize(AuthenticationSchemes = "Bearer")]
|
||||
[SwaggerTag("Access request management - requires authentication")]
|
||||
public class AccessRequestsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<AccessRequestsController> _logger;
|
||||
|
||||
public AccessRequestsController(IMediator mediator, ILogger<AccessRequestsController> logger)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new access request.
|
||||
/// VI: Tạo yêu cầu truy cập mới.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[SwaggerOperation(Summary = "Create access request", OperationId = "CreateAccessRequest")]
|
||||
[SwaggerResponse(StatusCodes.Status201Created, "Request created", typeof(ApiResponse<AccessRequestResponse>))]
|
||||
public async Task<IActionResult> CreateAccessRequest(
|
||||
[FromBody] CreateAccessRequestRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var command = new CreateAccessRequestCommand(
|
||||
request.RequesterId,
|
||||
request.ResourceType,
|
||||
request.ResourceId,
|
||||
request.RequestedPermission,
|
||||
request.Justification,
|
||||
request.Priority,
|
||||
request.ApproverIds);
|
||||
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetAccessRequestById), new { id = result.Id },
|
||||
ApiResponse<AccessRequestResponse>.Ok(new AccessRequestResponse
|
||||
{
|
||||
Id = result.Id,
|
||||
Status = result.Status,
|
||||
CreatedAt = result.CreatedAt
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get access request by ID.
|
||||
/// VI: Lấy yêu cầu truy cập theo ID.
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}")]
|
||||
[SwaggerOperation(Summary = "Get access request by ID", OperationId = "GetAccessRequestById")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Request found", typeof(ApiResponse<AccessRequestDto>))]
|
||||
[SwaggerResponse(StatusCodes.Status404NotFound, "Request not found")]
|
||||
public async Task<IActionResult> GetAccessRequestById(
|
||||
[FromRoute] Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new GetAccessRequestByIdQuery(id), cancellationToken);
|
||||
if (result == null)
|
||||
return NotFound(ApiResponse<AccessRequestDto>.Fail("REQUEST_NOT_FOUND", $"Access request {id} not found."));
|
||||
|
||||
return Ok(ApiResponse<AccessRequestDto>.Ok(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get my access requests.
|
||||
/// VI: Lấy yêu cầu truy cập của tôi.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[SwaggerOperation(Summary = "Get my access requests", OperationId = "GetMyAccessRequests")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Requests returned", typeof(ApiResponse<IEnumerable<AccessRequestDto>>))]
|
||||
public async Task<IActionResult> GetMyAccessRequests(
|
||||
[FromQuery] Guid requesterId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new GetMyAccessRequestsQuery(requesterId), cancellationToken);
|
||||
return Ok(ApiResponse<IEnumerable<AccessRequestDto>>.Ok(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get pending approvals for me.
|
||||
/// VI: Lấy các yêu cầu đang chờ tôi phê duyệt.
|
||||
/// </summary>
|
||||
[HttpGet("pending")]
|
||||
[SwaggerOperation(Summary = "Get pending approvals", OperationId = "GetPendingApprovals")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Pending approvals returned", typeof(ApiResponse<IEnumerable<AccessRequestDto>>))]
|
||||
public async Task<IActionResult> GetPendingApprovals(
|
||||
[FromQuery] Guid approverId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new GetPendingApprovalsQuery(approverId), cancellationToken);
|
||||
return Ok(ApiResponse<IEnumerable<AccessRequestDto>>.Ok(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Submit access request for approval.
|
||||
/// VI: Gửi yêu cầu truy cập để phê duyệt.
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/submit")]
|
||||
[SwaggerOperation(Summary = "Submit access request", OperationId = "SubmitAccessRequest")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Request submitted")]
|
||||
[SwaggerResponse(StatusCodes.Status404NotFound, "Request not found")]
|
||||
public async Task<IActionResult> SubmitAccessRequest(
|
||||
[FromRoute] Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediator.Send(new SubmitAccessRequestCommand(id), cancellationToken);
|
||||
return Ok(ApiResponse<object>.Ok(new { Message = "Access request submitted successfully." }));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("not found"))
|
||||
{
|
||||
return NotFound(ApiResponse<object>.Fail("REQUEST_NOT_FOUND", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approve access request.
|
||||
/// VI: Phê duyệt yêu cầu truy cập.
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/approve")]
|
||||
[SwaggerOperation(Summary = "Approve access request", OperationId = "ApproveAccessRequest")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Request approved")]
|
||||
[SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid operation")]
|
||||
public async Task<IActionResult> ApproveAccessRequest(
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] ApproveRejectRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediator.Send(new ApproveAccessRequestCommand(id, request.ApproverId, request.Comments), cancellationToken);
|
||||
return Ok(ApiResponse<object>.Ok(new { Message = "Access request approved." }));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("not found"))
|
||||
{
|
||||
return NotFound(ApiResponse<object>.Fail("REQUEST_NOT_FOUND", ex.Message));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ApiResponse<object>.Fail("INVALID_OPERATION", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Reject access request.
|
||||
/// VI: Từ chối yêu cầu truy cập.
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/reject")]
|
||||
[SwaggerOperation(Summary = "Reject access request", OperationId = "RejectAccessRequest")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Request rejected")]
|
||||
public async Task<IActionResult> RejectAccessRequest(
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] ApproveRejectRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediator.Send(new RejectAccessRequestCommand(id, request.ApproverId, request.Comments), cancellationToken);
|
||||
return Ok(ApiResponse<object>.Ok(new { Message = "Access request rejected." }));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("not found"))
|
||||
{
|
||||
return NotFound(ApiResponse<object>.Fail("REQUEST_NOT_FOUND", ex.Message));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ApiResponse<object>.Fail("INVALID_OPERATION", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Cancel access request.
|
||||
/// VI: Hủy yêu cầu truy cập.
|
||||
/// </summary>
|
||||
[HttpDelete("{id:guid}")]
|
||||
[SwaggerOperation(Summary = "Cancel access request", OperationId = "CancelAccessRequest")]
|
||||
[SwaggerResponse(StatusCodes.Status200OK, "Request cancelled")]
|
||||
public async Task<IActionResult> CancelAccessRequest(
|
||||
[FromRoute] Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediator.Send(new CancelAccessRequestCommand(id), cancellationToken);
|
||||
return Ok(ApiResponse<object>.Ok(new { Message = "Access request cancelled." }));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("not found"))
|
||||
{
|
||||
return NotFound(ApiResponse<object>.Fail("REQUEST_NOT_FOUND", ex.Message));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ApiResponse<object>.Fail("INVALID_OPERATION", ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Request/Response Models
|
||||
|
||||
public class CreateAccessRequestRequest
|
||||
{
|
||||
public Guid RequesterId { get; set; }
|
||||
/// <example>Organization</example>
|
||||
public string ResourceType { get; set; } = string.Empty;
|
||||
public Guid ResourceId { get; set; }
|
||||
/// <example>Admin</example>
|
||||
public string RequestedPermission { get; set; } = string.Empty;
|
||||
public string? Justification { get; set; }
|
||||
/// <example>2</example>
|
||||
public int? Priority { get; set; } // 1=Low, 2=Medium, 3=High, 4=Critical
|
||||
public List<Guid> ApproverIds { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ApproveRejectRequest
|
||||
{
|
||||
public Guid ApproverId { get; set; }
|
||||
public string? Comments { get; set; }
|
||||
}
|
||||
|
||||
public class AccessRequestResponse
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1,206 @@
|
||||
using IamService.Domain.Events;
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access request aggregate root for managing access request workflows.
|
||||
/// VI: Aggregate root yêu cầu truy cập để quản lý workflow yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public class AccessRequest : Entity, IAggregateRoot
|
||||
{
|
||||
private Guid _requesterId;
|
||||
private string _resourceType = null!;
|
||||
private Guid _resourceId;
|
||||
private string _requestedPermission = null!;
|
||||
private string? _justification;
|
||||
private AccessRequestStatus _status = null!;
|
||||
private AccessRequestPriority _priority = null!;
|
||||
private DateTime _createdAt;
|
||||
private DateTime? _submittedAt;
|
||||
private DateTime? _resolvedAt;
|
||||
private DateTime? _expiresAt;
|
||||
|
||||
private readonly List<AccessRequestApprover> _approvers = [];
|
||||
|
||||
#region Properties
|
||||
|
||||
public Guid RequesterId => _requesterId;
|
||||
public string ResourceType => _resourceType;
|
||||
public Guid ResourceId => _resourceId;
|
||||
public string RequestedPermission => _requestedPermission;
|
||||
public string? Justification => _justification;
|
||||
public AccessRequestStatus Status => _status;
|
||||
public AccessRequestPriority Priority => _priority;
|
||||
public DateTime CreatedAt => _createdAt;
|
||||
public DateTime? SubmittedAt => _submittedAt;
|
||||
public DateTime? ResolvedAt => _resolvedAt;
|
||||
public DateTime? ExpiresAt => _expiresAt;
|
||||
public IReadOnlyCollection<AccessRequestApprover> Approvers => _approvers.AsReadOnly();
|
||||
|
||||
#endregion
|
||||
|
||||
protected AccessRequest() { }
|
||||
|
||||
private AccessRequest(
|
||||
Guid requesterId,
|
||||
string resourceType,
|
||||
Guid resourceId,
|
||||
string requestedPermission,
|
||||
string? justification,
|
||||
AccessRequestPriority? priority)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
_requesterId = requesterId;
|
||||
_resourceType = resourceType;
|
||||
_resourceId = resourceId;
|
||||
_requestedPermission = requestedPermission;
|
||||
_justification = justification;
|
||||
_priority = priority ?? AccessRequestPriority.Medium;
|
||||
_status = AccessRequestStatus.Draft;
|
||||
_createdAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new AccessRequestCreatedEvent(Id, requesterId, resourceType, resourceId, requestedPermission));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Factory method to create access request.
|
||||
/// VI: Factory method để tạo yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public static AccessRequest Create(
|
||||
Guid requesterId,
|
||||
string resourceType,
|
||||
Guid resourceId,
|
||||
string requestedPermission,
|
||||
string? justification = null,
|
||||
AccessRequestPriority? priority = null)
|
||||
{
|
||||
if (requesterId == Guid.Empty)
|
||||
throw new ArgumentException("Requester ID cannot be empty", nameof(requesterId));
|
||||
if (string.IsNullOrWhiteSpace(resourceType))
|
||||
throw new ArgumentException("Resource type cannot be empty", nameof(resourceType));
|
||||
if (resourceId == Guid.Empty)
|
||||
throw new ArgumentException("Resource ID cannot be empty", nameof(resourceId));
|
||||
if (string.IsNullOrWhiteSpace(requestedPermission))
|
||||
throw new ArgumentException("Requested permission cannot be empty", nameof(requestedPermission));
|
||||
|
||||
return new AccessRequest(requesterId, resourceType, resourceId, requestedPermission, justification, priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Add an approver to the approval chain.
|
||||
/// VI: Thêm người phê duyệt vào chuỗi phê duyệt.
|
||||
/// </summary>
|
||||
public AccessRequestApprover AddApprover(Guid userId)
|
||||
{
|
||||
if (_status != AccessRequestStatus.Draft)
|
||||
throw new InvalidOperationException("Cannot add approvers after request is submitted");
|
||||
|
||||
var order = _approvers.Count + 1;
|
||||
var approver = new AccessRequestApprover(Id, userId, order);
|
||||
_approvers.Add(approver);
|
||||
return approver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Submit request for approval.
|
||||
/// VI: Gửi yêu cầu để phê duyệt.
|
||||
/// </summary>
|
||||
public void Submit(int expirationDays = 7)
|
||||
{
|
||||
if (_status != AccessRequestStatus.Draft)
|
||||
throw new InvalidOperationException("Only draft requests can be submitted");
|
||||
|
||||
if (_approvers.Count == 0)
|
||||
throw new InvalidOperationException("At least one approver is required");
|
||||
|
||||
_status = AccessRequestStatus.Pending;
|
||||
_submittedAt = DateTime.UtcNow;
|
||||
_expiresAt = DateTime.UtcNow.AddDays(expirationDays);
|
||||
|
||||
AddDomainEvent(new AccessRequestSubmittedEvent(Id, _requesterId, _approvers.Select(a => a.UserId).ToList()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approve the request by an approver.
|
||||
/// VI: Phê duyệt yêu cầu bởi người phê duyệt.
|
||||
/// </summary>
|
||||
public void Approve(Guid approverId, string? comments = null)
|
||||
{
|
||||
if (_status != AccessRequestStatus.Pending)
|
||||
throw new InvalidOperationException("Only pending requests can be approved");
|
||||
|
||||
var approver = _approvers.FirstOrDefault(a => a.UserId == approverId && a.Status == ApproverStatus.Pending);
|
||||
if (approver == null)
|
||||
throw new InvalidOperationException("User is not a pending approver for this request");
|
||||
|
||||
approver.Approve(comments);
|
||||
|
||||
// EN: Check if all approvers have approved
|
||||
// VI: Kiểm tra xem tất cả approvers đã approve chưa
|
||||
if (_approvers.All(a => a.Status == ApproverStatus.Approved))
|
||||
{
|
||||
_status = AccessRequestStatus.Approved;
|
||||
_resolvedAt = DateTime.UtcNow;
|
||||
AddDomainEvent(new AccessRequestApprovedEvent(Id, _requesterId, _resourceType, _resourceId, _requestedPermission));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Reject the request.
|
||||
/// VI: Từ chối yêu cầu.
|
||||
/// </summary>
|
||||
public void Reject(Guid approverId, string? reason = null)
|
||||
{
|
||||
if (_status != AccessRequestStatus.Pending)
|
||||
throw new InvalidOperationException("Only pending requests can be rejected");
|
||||
|
||||
var approver = _approvers.FirstOrDefault(a => a.UserId == approverId && a.Status == ApproverStatus.Pending);
|
||||
if (approver == null)
|
||||
throw new InvalidOperationException("User is not a pending approver for this request");
|
||||
|
||||
approver.Reject(reason);
|
||||
_status = AccessRequestStatus.Rejected;
|
||||
_resolvedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new AccessRequestRejectedEvent(Id, _requesterId, approverId, reason));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Cancel the request (by requester).
|
||||
/// VI: Hủy yêu cầu (bởi người yêu cầu).
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
if (_status.IsTerminal)
|
||||
throw new InvalidOperationException("Cannot cancel a terminal request");
|
||||
|
||||
_status = AccessRequestStatus.Cancelled;
|
||||
_resolvedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Mark request as expired.
|
||||
/// VI: Đánh dấu yêu cầu đã hết hạn.
|
||||
/// </summary>
|
||||
public void Expire()
|
||||
{
|
||||
if (_status != AccessRequestStatus.Pending)
|
||||
throw new InvalidOperationException("Only pending requests can expire");
|
||||
|
||||
_status = AccessRequestStatus.Expired;
|
||||
_resolvedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update justification.
|
||||
/// VI: Cập nhật lý do.
|
||||
/// </summary>
|
||||
public void UpdateJustification(string justification)
|
||||
{
|
||||
if (_status != AccessRequestStatus.Draft)
|
||||
throw new InvalidOperationException("Cannot update submitted request");
|
||||
|
||||
_justification = justification;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approver for an access request.
|
||||
/// VI: Người phê duyệt cho yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public class AccessRequestApprover : Entity
|
||||
{
|
||||
private Guid _requestId;
|
||||
private Guid _userId;
|
||||
private int _order;
|
||||
private ApproverStatus _status;
|
||||
private DateTime? _respondedAt;
|
||||
private string? _comments;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access request ID.
|
||||
/// VI: ID yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public Guid RequestId => _requestId;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approver user ID.
|
||||
/// VI: ID user phê duyệt.
|
||||
/// </summary>
|
||||
public Guid UserId => _userId;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Order in approval chain (1 = first).
|
||||
/// VI: Thứ tự trong chuỗi phê duyệt (1 = đầu tiên).
|
||||
/// </summary>
|
||||
public int Order => _order;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approver status.
|
||||
/// VI: Trạng thái phê duyệt.
|
||||
/// </summary>
|
||||
public ApproverStatus Status => _status;
|
||||
|
||||
/// <summary>
|
||||
/// EN: When approver responded.
|
||||
/// VI: Thời gian phản hồi.
|
||||
/// </summary>
|
||||
public DateTime? RespondedAt => _respondedAt;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approver comments.
|
||||
/// VI: Nhận xét của người phê duyệt.
|
||||
/// </summary>
|
||||
public string? Comments => _comments;
|
||||
|
||||
protected AccessRequestApprover()
|
||||
{
|
||||
_status = ApproverStatus.Pending;
|
||||
}
|
||||
|
||||
public AccessRequestApprover(Guid requestId, Guid userId, int order)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
_requestId = requestId;
|
||||
_userId = userId;
|
||||
_order = order;
|
||||
_status = ApproverStatus.Pending;
|
||||
}
|
||||
|
||||
public void Approve(string? comments = null)
|
||||
{
|
||||
_status = ApproverStatus.Approved;
|
||||
_respondedAt = DateTime.UtcNow;
|
||||
_comments = comments;
|
||||
}
|
||||
|
||||
public void Reject(string? comments = null)
|
||||
{
|
||||
_status = ApproverStatus.Rejected;
|
||||
_respondedAt = DateTime.UtcNow;
|
||||
_comments = comments;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approver status enumeration.
|
||||
/// VI: Enumeration trạng thái phê duyệt.
|
||||
/// </summary>
|
||||
public class ApproverStatus : Enumeration
|
||||
{
|
||||
public static readonly ApproverStatus Pending = new(1, nameof(Pending));
|
||||
public static readonly ApproverStatus Approved = new(2, nameof(Approved));
|
||||
public static readonly ApproverStatus Rejected = new(3, nameof(Rejected));
|
||||
|
||||
public ApproverStatus(int id, string name) : base(id, name) { }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access request priority enumeration.
|
||||
/// VI: Enumeration mức độ ưu tiên yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public class AccessRequestPriority : Enumeration
|
||||
{
|
||||
public static readonly AccessRequestPriority Low = new(1, nameof(Low));
|
||||
public static readonly AccessRequestPriority Medium = new(2, nameof(Medium));
|
||||
public static readonly AccessRequestPriority High = new(3, nameof(High));
|
||||
public static readonly AccessRequestPriority Critical = new(4, nameof(Critical));
|
||||
|
||||
public AccessRequestPriority(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
|
||||
public static IEnumerable<AccessRequestPriority> GetAll() => [Low, Medium, High, Critical];
|
||||
|
||||
public static AccessRequestPriority? FromId(int id) => id switch
|
||||
{
|
||||
1 => Low,
|
||||
2 => Medium,
|
||||
3 => High,
|
||||
4 => Critical,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access request status enumeration.
|
||||
/// VI: Enumeration trạng thái yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public class AccessRequestStatus : Enumeration
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Request is being drafted, not yet submitted.
|
||||
/// VI: Request đang được soạn, chưa gửi.
|
||||
/// </summary>
|
||||
public static readonly AccessRequestStatus Draft = new(1, nameof(Draft));
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request submitted and waiting for approval.
|
||||
/// VI: Request đã gửi và đang chờ phê duyệt.
|
||||
/// </summary>
|
||||
public static readonly AccessRequestStatus Pending = new(2, nameof(Pending));
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request approved by all approvers.
|
||||
/// VI: Request được phê duyệt bởi tất cả approver.
|
||||
/// </summary>
|
||||
public static readonly AccessRequestStatus Approved = new(3, nameof(Approved));
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request rejected by an approver.
|
||||
/// VI: Request bị từ chối bởi approver.
|
||||
/// </summary>
|
||||
public static readonly AccessRequestStatus Rejected = new(4, nameof(Rejected));
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request cancelled by requester.
|
||||
/// VI: Request bị hủy bởi người yêu cầu.
|
||||
/// </summary>
|
||||
public static readonly AccessRequestStatus Cancelled = new(5, nameof(Cancelled));
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request expired due to timeout.
|
||||
/// VI: Request hết hạn do quá thời gian.
|
||||
/// </summary>
|
||||
public static readonly AccessRequestStatus Expired = new(6, nameof(Expired));
|
||||
|
||||
public AccessRequestStatus(int id, string name) : base(id, name)
|
||||
{
|
||||
}
|
||||
|
||||
public static IEnumerable<AccessRequestStatus> GetAll() =>
|
||||
[
|
||||
Draft,
|
||||
Pending,
|
||||
Approved,
|
||||
Rejected,
|
||||
Cancelled,
|
||||
Expired
|
||||
];
|
||||
|
||||
public static AccessRequestStatus? FromId(int id) => id switch
|
||||
{
|
||||
1 => Draft,
|
||||
2 => Pending,
|
||||
3 => Approved,
|
||||
4 => Rejected,
|
||||
5 => Cancelled,
|
||||
6 => Expired,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public bool IsTerminal => this == Approved || this == Rejected || this == Cancelled || this == Expired;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Repository interface for AccessRequest aggregate.
|
||||
/// VI: Interface repository cho AccessRequest aggregate.
|
||||
/// </summary>
|
||||
public interface IAccessRequestRepository : IRepository<AccessRequest>
|
||||
{
|
||||
AccessRequest Add(AccessRequest request);
|
||||
|
||||
void Update(AccessRequest request);
|
||||
|
||||
Task<AccessRequest?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AccessRequest?> GetByIdWithApproversAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IEnumerable<AccessRequest>> GetByRequesterIdAsync(Guid requesterId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IEnumerable<AccessRequest>> GetPendingByApproverIdAsync(Guid approverId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IEnumerable<AccessRequest>> GetExpiredRequestsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event when access request is created.
|
||||
/// VI: Event khi yêu cầu truy cập được tạo.
|
||||
/// </summary>
|
||||
public record AccessRequestCreatedEvent(
|
||||
Guid RequestId,
|
||||
Guid RequesterId,
|
||||
string ResourceType,
|
||||
Guid ResourceId,
|
||||
string RequestedPermission) : IDomainEvent
|
||||
{
|
||||
public DateTime OccurredOn { get; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event when access request is submitted for approval.
|
||||
/// VI: Event khi yêu cầu truy cập được gửi để phê duyệt.
|
||||
/// </summary>
|
||||
public record AccessRequestSubmittedEvent(
|
||||
Guid RequestId,
|
||||
Guid RequesterId,
|
||||
IReadOnlyList<Guid> ApproverIds) : IDomainEvent
|
||||
{
|
||||
public DateTime OccurredOn { get; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event when access request is approved.
|
||||
/// VI: Event khi yêu cầu truy cập được phê duyệt.
|
||||
/// </summary>
|
||||
public record AccessRequestApprovedEvent(
|
||||
Guid RequestId,
|
||||
Guid RequesterId,
|
||||
string ResourceType,
|
||||
Guid ResourceId,
|
||||
string Permission) : IDomainEvent
|
||||
{
|
||||
public DateTime OccurredOn { get; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event when access request is rejected.
|
||||
/// VI: Event khi yêu cầu truy cập bị từ chối.
|
||||
/// </summary>
|
||||
public record AccessRequestRejectedEvent(
|
||||
Guid RequestId,
|
||||
Guid RequesterId,
|
||||
Guid RejectedByUserId,
|
||||
string? Reason) : IDomainEvent
|
||||
{
|
||||
public DateTime OccurredOn { get; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using IamService.Domain.AggregatesModel.RoleAggregate;
|
||||
using IamService.Domain.AggregatesModel.OrganizationAggregate;
|
||||
using IamService.Domain.AggregatesModel.GroupAggregate;
|
||||
using IamService.Domain.AggregatesModel.VerificationAggregate;
|
||||
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
using IamService.Domain.SeedWork;
|
||||
using IamService.Infrastructure.Email;
|
||||
using IamService.Infrastructure.IdentityServer;
|
||||
@@ -153,6 +154,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IOrganizationRepository, OrganizationRepository>();
|
||||
services.AddScoped<IGroupRepository, GroupRepository>();
|
||||
services.AddScoped<IIdentityVerificationRepository, IdentityVerificationRepository>();
|
||||
services.AddScoped<IAccessRequestRepository, AccessRequestRepository>();
|
||||
services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<IamServiceContext>());
|
||||
|
||||
// EN: Configure Redis caching (skip in Testing environment)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
|
||||
namespace IamService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Entity configuration for AccessRequest.
|
||||
/// VI: Cấu hình entity cho AccessRequest.
|
||||
/// </summary>
|
||||
public class AccessRequestEntityConfiguration : IEntityTypeConfiguration<AccessRequest>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AccessRequest> builder)
|
||||
{
|
||||
builder.ToTable("AccessRequests");
|
||||
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Id)
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid>("_requesterId")
|
||||
.HasColumnName("RequesterId")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string>("_resourceType")
|
||||
.HasColumnName("ResourceType")
|
||||
.HasMaxLength(100)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<Guid>("_resourceId")
|
||||
.HasColumnName("ResourceId")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string>("_requestedPermission")
|
||||
.HasColumnName("RequestedPermission")
|
||||
.HasMaxLength(100)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string?>("_justification")
|
||||
.HasColumnName("Justification")
|
||||
.HasMaxLength(2000);
|
||||
|
||||
builder.Property<DateTime>("_createdAt")
|
||||
.HasColumnName("CreatedAt")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime?>("_submittedAt")
|
||||
.HasColumnName("SubmittedAt");
|
||||
|
||||
builder.Property<DateTime?>("_resolvedAt")
|
||||
.HasColumnName("ResolvedAt");
|
||||
|
||||
builder.Property<DateTime?>("_expiresAt")
|
||||
.HasColumnName("ExpiresAt");
|
||||
|
||||
// EN: Status with value conversion to use proper static instances
|
||||
// VI: Status với value conversion để dùng đúng static instances
|
||||
builder.Property<AccessRequestStatus>("_status")
|
||||
.HasColumnName("StatusId")
|
||||
.HasConversion(
|
||||
v => v.Id,
|
||||
v => AccessRequestStatus.FromId(v) ?? AccessRequestStatus.Draft);
|
||||
|
||||
// EN: Priority with value conversion
|
||||
// VI: Priority với value conversion
|
||||
builder.Property<AccessRequestPriority>("_priority")
|
||||
.HasColumnName("PriorityId")
|
||||
.HasConversion(
|
||||
v => v.Id,
|
||||
v => AccessRequestPriority.FromId(v) ?? AccessRequestPriority.Medium);
|
||||
|
||||
// EN: Approvers collection
|
||||
// VI: Collection approvers
|
||||
builder.HasMany(x => x.Approvers)
|
||||
.WithOne()
|
||||
.HasForeignKey("_requestId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// EN: Ignore domain events
|
||||
// VI: Bỏ qua domain events
|
||||
builder.Ignore(x => x.DomainEvents);
|
||||
|
||||
// EN: Indexes
|
||||
// VI: Indexes
|
||||
builder.HasIndex("_requesterId").HasDatabaseName("IX_AccessRequests_RequesterId");
|
||||
builder.HasIndex("_resourceType", "_resourceId").HasDatabaseName("IX_AccessRequests_Resource");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// EN: Entity configuration for AccessRequestApprover.
|
||||
/// VI: Cấu hình entity cho AccessRequestApprover.
|
||||
/// </summary>
|
||||
public class AccessRequestApproverEntityConfiguration : IEntityTypeConfiguration<AccessRequestApprover>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AccessRequestApprover> builder)
|
||||
{
|
||||
builder.ToTable("AccessRequestApprovers");
|
||||
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Id)
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid>("_requestId")
|
||||
.HasColumnName("RequestId")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<Guid>("_userId")
|
||||
.HasColumnName("UserId")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<int>("_order")
|
||||
.HasColumnName("ApprovalOrder")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime?>("_respondedAt")
|
||||
.HasColumnName("RespondedAt");
|
||||
|
||||
builder.Property<string?>("_comments")
|
||||
.HasColumnName("Comments")
|
||||
.HasMaxLength(1000);
|
||||
|
||||
// EN: Status with value conversion
|
||||
// VI: Status với value conversion
|
||||
builder.Property<ApproverStatus>("_status")
|
||||
.HasColumnName("StatusId")
|
||||
.HasConversion(
|
||||
v => v.Id,
|
||||
v => v == 1 ? ApproverStatus.Pending : v == 2 ? ApproverStatus.Approved : ApproverStatus.Rejected);
|
||||
|
||||
builder.Ignore(x => x.DomainEvents);
|
||||
|
||||
builder.HasIndex("_userId").HasDatabaseName("IX_AccessRequestApprovers_UserId");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using IamService.Domain.AggregatesModel.RoleAggregate;
|
||||
using IamService.Domain.AggregatesModel.OrganizationAggregate;
|
||||
using IamService.Domain.AggregatesModel.GroupAggregate;
|
||||
using IamService.Domain.AggregatesModel.VerificationAggregate;
|
||||
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Infrastructure;
|
||||
@@ -112,6 +113,18 @@ public class IamServiceContext : IdentityDbContext<ApplicationUser, ApplicationR
|
||||
/// </summary>
|
||||
public DbSet<ProfileAttributeType> ProfileAttributeTypes { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access requests table.
|
||||
/// VI: Bảng yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public DbSet<AccessRequest> AccessRequests { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Access request approvers table.
|
||||
/// VI: Bảng người phê duyệt yêu cầu truy cập.
|
||||
/// </summary>
|
||||
public DbSet<AccessRequestApprover> AccessRequestApprovers { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check if there's an active transaction.
|
||||
/// VI: Kiểm tra xem có transaction đang hoạt động không.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Repository implementation for AccessRequest aggregate.
|
||||
/// VI: Repository implementation cho AccessRequest aggregate.
|
||||
/// </summary>
|
||||
public class AccessRequestRepository : IAccessRequestRepository
|
||||
{
|
||||
private readonly IamServiceContext _context;
|
||||
|
||||
public IUnitOfWork UnitOfWork => _context;
|
||||
|
||||
public AccessRequestRepository(IamServiceContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public AccessRequest Add(AccessRequest request)
|
||||
{
|
||||
return _context.AccessRequests.Add(request).Entity;
|
||||
}
|
||||
|
||||
public void Update(AccessRequest request)
|
||||
{
|
||||
_context.Entry(request).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public async Task<AccessRequest?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.AccessRequests
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<AccessRequest?> GetByIdWithApproversAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.AccessRequests
|
||||
.Include(x => x.Approvers)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AccessRequest>> GetByRequesterIdAsync(
|
||||
Guid requesterId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.AccessRequests
|
||||
.Include(x => x.Approvers)
|
||||
.Where(x => EF.Property<Guid>(x, "_requesterId") == requesterId)
|
||||
.OrderByDescending(x => EF.Property<DateTime>(x, "_createdAt"))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AccessRequest>> GetPendingByApproverIdAsync(
|
||||
Guid approverId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.AccessRequests
|
||||
.Include(x => x.Approvers)
|
||||
.Where(x => x.Approvers.Any(a =>
|
||||
EF.Property<Guid>(a, "_userId") == approverId &&
|
||||
EF.Property<ApproverStatus>(a, "_status") == ApproverStatus.Pending))
|
||||
.Where(x => EF.Property<AccessRequestStatus>(x, "_status") == AccessRequestStatus.Pending)
|
||||
.OrderByDescending(x => EF.Property<DateTime>(x, "_createdAt"))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AccessRequest>> GetExpiredRequestsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return await _context.AccessRequests
|
||||
.Where(x => EF.Property<AccessRequestStatus>(x, "_status") == AccessRequestStatus.Pending)
|
||||
.Where(x => EF.Property<DateTime?>(x, "_expiresAt") < now)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user