feat: Introduce new Access Management and Governance APIs in IAM Service

- Added Access Requests, Access Reviews, Privileged Access Management, Audit Log, and Compliance APIs to enhance access management and governance capabilities.
- Updated the DbContext to include new entities for AuditLog and ComplianceReport, improving data handling for compliance and auditing.
- Enhanced Dependency Injection to support new repositories for the added functionalities, streamlining service operations.
This commit is contained in:
Ho Ngoc Hai
2026-01-14 19:26:26 +07:00
parent 8b7db56b79
commit f19a995b0d
21 changed files with 1024 additions and 53 deletions

View File

@@ -290,57 +290,72 @@ graph TD
| `POST` | `/api/v1/verifications/email` | Yêu cầu xác thực email | ✅ |
| `POST` | `/api/v1/verifications/{id}/confirm` | Xác nhận với OTP code | ✅ |
### 4.10 Access Management APIs (Planned)
### 4.10 Access Requests APIs ✅ (New in Phase 3A)
| Method | Endpoint | Mô tả | Auth |
|--------|----------|-------|------|
| `POST` | `/api/v1/access-requests` | Tạo yêu cầu truy cập mới | ✅ |
| `GET` | `/api/v1/access-requests` | Lấy danh sách requests của user | ✅ |
| `GET` | `/api/v1/access-requests/{id}` | Lấy request theo ID | ✅ |
| `POST` | `/api/v1/access-requests/{id}/submit` | Submit request để phê duyệt | ✅ |
| `POST` | `/api/v1/access-requests/{id}/approve` | Phê duyệt request | ✅ |
| `POST` | `/api/v1/access-requests/{id}/reject` | Từ chối request | ✅ |
| `DELETE` | `/api/v1/access-requests/{id}` | Hủy request | ✅ |
| `GET` | `/api/v1/access-requests/pending` | Lấy requests đang chờ phê duyệt | ✅ |
### 4.11 Access Reviews APIs ✅ (New in Phase 3B)
| Method | Endpoint | Mô tả | Auth |
|--------|----------|-------|------|
| `POST` | `/api/v1/access-reviews` | Tạo access review mới | ✅ |
| `GET` | `/api/v1/access-reviews/{id}` | Lấy review theo ID | ✅ |
| `POST` | `/api/v1/access-reviews/{id}/items` | Thêm item vào review | ✅ |
| `POST` | `/api/v1/access-reviews/{id}/start` | Bắt đầu review campaign | ✅ |
| `POST` | `/api/v1/access-reviews/{id}/items/{itemId}/review` | Certify/Revoke item | ✅ |
| `POST` | `/api/v1/access-reviews/{id}/complete` | Hoàn thành review | ✅ |
### 4.12 Privileged Access Management (PAM) APIs ✅ (New in Phase 3B)
| Method | Endpoint | Mô tả | Auth |
|--------|----------|-------|------|
| `POST` | `/api/v1/privileged-access/request` | Yêu cầu JIT elevated access | ✅ |
| `GET` | `/api/v1/privileged-access/active` | Lấy grants đang active | ✅ |
| `POST` | `/api/v1/privileged-access/{id}/revoke` | Thu hồi privileged access | ✅ |
### 4.13 Audit Log APIs ✅ (New in Phase 4A)
| Method | Endpoint | Mô tả | Auth |
|--------|----------|-------|------|
| `GET` | `/api/v1/audit/logs` | Lấy audit logs (filtered, paginated) | ✅ |
### 4.14 Compliance APIs ✅ (New in Phase 4A)
| Method | Endpoint | Mô tả | Auth |
|--------|----------|-------|------|
| `POST` | `/api/v1/compliance/reports` | Tạo compliance report mới | ✅ |
| `GET` | `/api/v1/compliance/reports` | Lấy danh sách reports | ✅ |
| `GET` | `/api/v1/compliance/reports/{id}` | Lấy report chi tiết | ✅ |
| `POST` | `/api/v1/compliance/reports/{id}/complete` | Hoàn thành report | ✅ |
| `GET` | `/api/v1/compliance/violations` | Lấy violations chưa giải quyết | ✅ |
### 4.15 Governance APIs (Planned - Phase 4B)
> [!NOTE]
> Các APIs dưới đây là tính năng **đang được lên kế hoạch**, chưa triển khai.
> Các APIs dưới đây là tính năng **đang được lên kế hoạch** cho Phase 4B.
```
# Access Requests
GET /api/v1/access/requests
POST /api/v1/access/requests
PUT /api/v1/access/requests/:id/approve
PUT /api/v1/access/requests/:id/reject
# Access Reviews
GET /api/v1/access/reviews
POST /api/v1/access/reviews
POST /api/v1/access/reviews/:id/start
POST /api/v1/access/reviews/:id/complete
GET /api/v1/access/reviews/:id/items
# Access Analytics
GET /api/v1/access/analytics/usage
GET /api/v1/access/analytics/permissions
GET /api/v1/access/analytics/risks
```
### 4.8 Governance APIs (Planned)
> [!NOTE]
> Các APIs dưới đây là tính năng **đang được lên kế hoạch**, chưa triển khai.
```
# Compliance Reports
GET /api/v1/governance/compliance/reports
POST /api/v1/governance/compliance/reports/generate
GET /api/v1/governance/compliance/reports/:id/export
# Policy Governance
GET /api/v1/governance/policies/templates
POST /api/v1/governance/policies/templates
GET /api/v1/governance/policies/:id/versions
POST /api/v1/governance/policies/:id/test
GET /api/v1/policies
POST /api/v1/policies
GET /api/v1/policies/:id/versions
POST /api/v1/policies/:id/activate
# Risk Management
GET /api/v1/governance/risk/scores
GET /api/v1/governance/risk/scores/:userId
POST /api/v1/governance/risk/calculate
GET /api/v1/risk/scores/:userId
POST /api/v1/risk/calculate
# Reporting
GET /api/v1/governance/reports/access-summary
GET /api/v1/governance/reports/user-activity
GET /api/v1/governance/reports/security-events
# Security Dashboard
GET /api/v1/dashboard/security
```
---
@@ -368,17 +383,24 @@ GET /api/v1/governance/reports/security-events
- ✅ Organization & Group management
- ✅ Profile management with extended attributes (ProfileAttribute entity)
### Phase 3: Access Management (Planned)
- 🔄 Access request/approval workflows
- 🔄 Access review & certification system
- 🔄 Access analytics
- 🔄 Privileged Access Management (PAM)
### Phase 3: Access Management ✅ (Completed)
- Access request/approval workflows (Create → Submit → Approve/Reject)
- Access review & certification system (Certify/Revoke decisions)
- ✅ Privileged Access Management (PAM) với JIT elevated access
- ✅ Entity configurations với EF Core Value Conversion
### Phase 4: Governance (Planned)
- 🔄 Compliance reporting engine
- 🔄 Policy governance & versioning
- 🔄 Risk scoring & management
- 🔄 Reporting dashboards
### Phase 4: Governance (In Progress)
#### Phase 4A: Audit & Compliance ✅ (Completed)
- `AuditLog` aggregate với 18 event types
- `ComplianceReport` aggregate (GDPR, SOC2, ISO27001, HIPAA)
- ✅ Audit log search & filtering
- ✅ Compliance report generation & violations tracking
#### Phase 4B: Policy & Risk (Planned)
- 🔄 PolicyTemplate aggregate với versioning
- 🔄 RiskScore aggregate & calculation
- 🔄 Security posture dashboard
### Phase 5: Advanced Features (Planned)
- 🔄 Workflow engine

View File

@@ -0,0 +1,33 @@
using MediatR;
using IamService.Domain.AggregatesModel.AuditAggregate;
namespace IamService.API.Application.Commands.Audit;
public record CreateAuditLogCommand(
int EventTypeId,
string ResourceType,
Guid? ActorId = null,
string? ActorEmail = null,
Guid? ResourceId = null,
string? Action = null,
string? Details = null,
string? IpAddress = null,
string? UserAgent = null,
bool Success = true) : IRequest<Guid>;
public class CreateAuditLogCommandHandler : IRequestHandler<CreateAuditLogCommand, Guid>
{
private readonly IAuditLogRepository _repository;
public CreateAuditLogCommandHandler(IAuditLogRepository repository) => _repository = repository;
public async Task<Guid> Handle(CreateAuditLogCommand request, CancellationToken cancellationToken)
{
var eventType = AuditEventType.FromId(request.EventTypeId) ?? AuditEventType.Login;
var log = AuditLog.Create(
eventType, request.ResourceType, request.ActorId, request.ActorEmail,
request.ResourceId, request.Action, request.Details, request.IpAddress, request.UserAgent, request.Success);
_repository.Add(log);
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return log.Id;
}
}

View File

@@ -0,0 +1,59 @@
using MediatR;
using IamService.Domain.AggregatesModel.ComplianceAggregate;
using IamService.Domain.Exceptions;
namespace IamService.API.Application.Commands.Compliance;
public record GenerateComplianceReportCommand(string Name, int ReportTypeId, Guid GeneratedByUserId) : IRequest<Guid>;
public record CompleteComplianceReportCommand(Guid ReportId, int TotalChecks, int PassedChecks, string? Summary = null) : IRequest<Unit>;
public record AddViolationCommand(Guid ReportId, string Rule, int SeverityId, string Description, string? Remediation = null) : IRequest<Guid>;
public class GenerateComplianceReportCommandHandler : IRequestHandler<GenerateComplianceReportCommand, Guid>
{
private readonly IComplianceReportRepository _repository;
public GenerateComplianceReportCommandHandler(IComplianceReportRepository repository) => _repository = repository;
public async Task<Guid> Handle(GenerateComplianceReportCommand request, CancellationToken cancellationToken)
{
var reportType = ComplianceReportType.FromId(request.ReportTypeId) ?? ComplianceReportType.GDPR;
var report = ComplianceReport.Create(request.Name, reportType, request.GeneratedByUserId);
report.StartGenerating();
_repository.Add(report);
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return report.Id;
}
}
public class CompleteComplianceReportCommandHandler : IRequestHandler<CompleteComplianceReportCommand, Unit>
{
private readonly IComplianceReportRepository _repository;
public CompleteComplianceReportCommandHandler(IComplianceReportRepository repository) => _repository = repository;
public async Task<Unit> Handle(CompleteComplianceReportCommand request, CancellationToken cancellationToken)
{
var report = await _repository.GetByIdWithViolationsAsync(request.ReportId, cancellationToken)
?? throw new DomainException($"Report {request.ReportId} not found.");
report.SetResults(request.TotalChecks, request.PassedChecks, request.Summary);
report.Complete();
_repository.Update(report);
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return Unit.Value;
}
}
public class AddViolationCommandHandler : IRequestHandler<AddViolationCommand, Guid>
{
private readonly IComplianceReportRepository _repository;
public AddViolationCommandHandler(IComplianceReportRepository repository) => _repository = repository;
public async Task<Guid> Handle(AddViolationCommand request, CancellationToken cancellationToken)
{
var report = await _repository.GetByIdWithViolationsAsync(request.ReportId, cancellationToken)
?? throw new DomainException($"Report {request.ReportId} not found.");
var severity = ViolationSeverity.FromId(request.SeverityId) ?? ViolationSeverity.Medium;
report.AddViolation(request.Rule, severity, request.Description, request.Remediation);
_repository.Update(report);
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return report.Violations.Last().Id;
}
}

View File

@@ -0,0 +1,45 @@
using MediatR;
using IamService.Domain.AggregatesModel.AuditAggregate;
namespace IamService.API.Application.Queries.Audit;
public record GetAuditLogsQuery(
DateTime? FromDate = null,
DateTime? ToDate = null,
int? EventTypeId = null,
Guid? ActorId = null,
string? ResourceType = null,
int Skip = 0,
int Take = 50) : IRequest<AuditLogsResult>;
public record AuditLogsResult(IEnumerable<AuditLogDto> Logs, long TotalCount);
public record AuditLogDto(
Guid Id,
string EventType,
Guid? ActorId,
string? ActorEmail,
string ResourceType,
Guid? ResourceId,
string? Action,
bool Success,
DateTime Timestamp);
public class GetAuditLogsQueryHandler : IRequestHandler<GetAuditLogsQuery, AuditLogsResult>
{
private readonly IAuditLogRepository _repository;
public GetAuditLogsQueryHandler(IAuditLogRepository repository) => _repository = repository;
public async Task<AuditLogsResult> Handle(GetAuditLogsQuery request, CancellationToken cancellationToken)
{
var logs = await _repository.SearchAsync(
request.FromDate, request.ToDate, request.EventTypeId, request.ActorId,
request.ResourceType, request.Skip, request.Take, cancellationToken);
var count = await _repository.GetCountAsync(request.FromDate, request.ToDate, cancellationToken);
var dtos = logs.Select(l => new AuditLogDto(
l.Id, l.EventType.Name, l.ActorId, l.ActorEmail, l.ResourceType,
l.ResourceId, l.Action, l.Success, l.Timestamp));
return new AuditLogsResult(dtos, count);
}
}

View File

@@ -0,0 +1,54 @@
using MediatR;
using IamService.Domain.AggregatesModel.ComplianceAggregate;
namespace IamService.API.Application.Queries.Compliance;
public record GetComplianceReportsQuery(int? ReportTypeId = null, int Take = 20) : IRequest<IEnumerable<ComplianceReportDto>>;
public record GetComplianceReportByIdQuery(Guid Id) : IRequest<ComplianceReportDetailDto?>;
public record GetUnresolvedViolationsQuery() : IRequest<IEnumerable<ViolationDto>>;
public record ComplianceReportDto(Guid Id, string Name, string ReportType, string Status, DateTime CreatedAt, double CompliancePercentage);
public record ComplianceReportDetailDto(Guid Id, string Name, string ReportType, string Status, DateTime CreatedAt, DateTime? CompletedAt,
int TotalChecks, int PassedChecks, int FailedChecks, double CompliancePercentage, string? Summary, IEnumerable<ViolationDto> Violations);
public record ViolationDto(Guid Id, string Rule, string Severity, string Description, string? Remediation, bool Resolved);
public class GetComplianceReportsQueryHandler : IRequestHandler<GetComplianceReportsQuery, IEnumerable<ComplianceReportDto>>
{
private readonly IComplianceReportRepository _repository;
public GetComplianceReportsQueryHandler(IComplianceReportRepository repository) => _repository = repository;
public async Task<IEnumerable<ComplianceReportDto>> Handle(GetComplianceReportsQuery request, CancellationToken ct)
{
var reports = request.ReportTypeId.HasValue
? await _repository.GetByTypeAsync(request.ReportTypeId.Value, request.Take, ct)
: await _repository.GetRecentAsync(request.Take, ct);
return reports.Select(r => new ComplianceReportDto(r.Id, r.Name, r.ReportType.Name, r.Status.Name, r.CreatedAt, r.CompliancePercentage));
}
}
public class GetComplianceReportByIdQueryHandler : IRequestHandler<GetComplianceReportByIdQuery, ComplianceReportDetailDto?>
{
private readonly IComplianceReportRepository _repository;
public GetComplianceReportByIdQueryHandler(IComplianceReportRepository repository) => _repository = repository;
public async Task<ComplianceReportDetailDto?> Handle(GetComplianceReportByIdQuery request, CancellationToken ct)
{
var r = await _repository.GetByIdWithViolationsAsync(request.Id, ct);
if (r == null) return null;
var violations = r.Violations.Select(v => new ViolationDto(v.Id, v.Rule, v.Severity.Name, v.Description, v.Remediation, v.Resolved));
return new ComplianceReportDetailDto(r.Id, r.Name, r.ReportType.Name, r.Status.Name, r.CreatedAt, r.CompletedAt,
r.TotalChecks, r.PassedChecks, r.FailedChecks, r.CompliancePercentage, r.Summary, violations);
}
}
public class GetUnresolvedViolationsQueryHandler : IRequestHandler<GetUnresolvedViolationsQuery, IEnumerable<ViolationDto>>
{
private readonly IComplianceReportRepository _repository;
public GetUnresolvedViolationsQueryHandler(IComplianceReportRepository repository) => _repository = repository;
public async Task<IEnumerable<ViolationDto>> Handle(GetUnresolvedViolationsQuery request, CancellationToken ct)
{
var violations = await _repository.GetUnresolvedViolationsAsync(ct);
return violations.Select(v => new ViolationDto(v.Id, v.Rule, v.Severity.Name, v.Description, v.Remediation, v.Resolved));
}
}

View File

@@ -0,0 +1,36 @@
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.Queries.Audit;
namespace IamService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/audit")]
[Authorize(AuthenticationSchemes = "Bearer")]
[SwaggerTag("Audit log management")]
public class AuditController : ControllerBase
{
private readonly IMediator _mediator;
public AuditController(IMediator mediator) => _mediator = mediator;
[HttpGet("logs")]
[SwaggerOperation(Summary = "Get audit logs", OperationId = "GetAuditLogs")]
public async Task<IActionResult> GetLogs(
[FromQuery] DateTime? fromDate,
[FromQuery] DateTime? toDate,
[FromQuery] int? eventTypeId,
[FromQuery] Guid? actorId,
[FromQuery] string? resourceType,
[FromQuery] int skip = 0,
[FromQuery] int take = 50,
CancellationToken ct = default)
{
var result = await _mediator.Send(new GetAuditLogsQuery(fromDate, toDate, eventTypeId, actorId, resourceType, skip, take), ct);
return Ok(ApiResponse<AuditLogsResult>.Ok(result));
}
}

View File

@@ -0,0 +1,73 @@
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.Compliance;
using IamService.API.Application.Queries.Compliance;
namespace IamService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/compliance")]
[Authorize(AuthenticationSchemes = "Bearer")]
[SwaggerTag("Compliance reporting")]
public class ComplianceController : ControllerBase
{
private readonly IMediator _mediator;
public ComplianceController(IMediator mediator) => _mediator = mediator;
[HttpPost("reports")]
[SwaggerOperation(Summary = "Generate compliance report", OperationId = "GenerateComplianceReport")]
public async Task<IActionResult> Generate([FromBody] GenerateReportRequest request, CancellationToken ct = default)
{
try
{
var id = await _mediator.Send(new GenerateComplianceReportCommand(request.Name, request.ReportTypeId, request.GeneratedByUserId), ct);
return Ok(ApiResponse<object>.Ok(new { ReportId = id, Status = "Generating" }));
}
catch (Exception ex) { return BadRequest(ApiResponse<object>.Fail("ERROR", ex.Message)); }
}
[HttpGet("reports")]
[SwaggerOperation(Summary = "Get compliance reports", OperationId = "GetComplianceReports")]
public async Task<IActionResult> GetReports([FromQuery] int? reportTypeId, [FromQuery] int take = 20, CancellationToken ct = default)
{
var reports = await _mediator.Send(new GetComplianceReportsQuery(reportTypeId, take), ct);
return Ok(ApiResponse<IEnumerable<ComplianceReportDto>>.Ok(reports));
}
[HttpGet("reports/{id:guid}")]
[SwaggerOperation(Summary = "Get compliance report by ID", OperationId = "GetComplianceReportById")]
public async Task<IActionResult> GetById([FromRoute] Guid id, CancellationToken ct = default)
{
var report = await _mediator.Send(new GetComplianceReportByIdQuery(id), ct);
if (report == null) return NotFound(ApiResponse<object>.Fail("NOT_FOUND", "Report not found"));
return Ok(ApiResponse<ComplianceReportDetailDto>.Ok(report));
}
[HttpGet("violations")]
[SwaggerOperation(Summary = "Get unresolved violations", OperationId = "GetUnresolvedViolations")]
public async Task<IActionResult> GetViolations(CancellationToken ct = default)
{
var violations = await _mediator.Send(new GetUnresolvedViolationsQuery(), ct);
return Ok(ApiResponse<IEnumerable<ViolationDto>>.Ok(violations));
}
[HttpPost("reports/{id:guid}/complete")]
[SwaggerOperation(Summary = "Complete compliance report", OperationId = "CompleteComplianceReport")]
public async Task<IActionResult> Complete([FromRoute] Guid id, [FromBody] CompleteReportRequest request, CancellationToken ct = default)
{
try
{
await _mediator.Send(new CompleteComplianceReportCommand(id, request.TotalChecks, request.PassedChecks, request.Summary), ct);
return Ok(ApiResponse<object>.Ok(new { Message = "Report completed" }));
}
catch (Exception ex) { return BadRequest(ApiResponse<object>.Fail("ERROR", ex.Message)); }
}
}
public record GenerateReportRequest(string Name, int ReportTypeId, Guid GeneratedByUserId);
public record CompleteReportRequest(int TotalChecks, int PassedChecks, string? Summary);

View File

@@ -0,0 +1,55 @@
using IamService.Domain.SeedWork;
namespace IamService.Domain.AggregatesModel.AuditAggregate;
/// <summary>
/// EN: Audit event type enumeration.
/// VI: Enumeration loại audit event.
/// </summary>
public class AuditEventType : Enumeration
{
// Authentication events
public static readonly AuditEventType Login = new(1, nameof(Login));
public static readonly AuditEventType Logout = new(2, nameof(Logout));
public static readonly AuditEventType LoginFailed = new(3, nameof(LoginFailed));
public static readonly AuditEventType PasswordChanged = new(4, nameof(PasswordChanged));
public static readonly AuditEventType PasswordResetRequested = new(5, nameof(PasswordResetRequested));
public static readonly AuditEventType TwoFactorEnabled = new(6, nameof(TwoFactorEnabled));
public static readonly AuditEventType TwoFactorDisabled = new(7, nameof(TwoFactorDisabled));
// User events
public static readonly AuditEventType UserCreated = new(10, nameof(UserCreated));
public static readonly AuditEventType UserUpdated = new(11, nameof(UserUpdated));
public static readonly AuditEventType UserDeleted = new(12, nameof(UserDeleted));
public static readonly AuditEventType UserLocked = new(13, nameof(UserLocked));
public static readonly AuditEventType UserUnlocked = new(14, nameof(UserUnlocked));
// Access events
public static readonly AuditEventType AccessRequested = new(20, nameof(AccessRequested));
public static readonly AuditEventType AccessGranted = new(21, nameof(AccessGranted));
public static readonly AuditEventType AccessRevoked = new(22, nameof(AccessRevoked));
public static readonly AuditEventType AccessDenied = new(23, nameof(AccessDenied));
public static readonly AuditEventType PrivilegedAccessGranted = new(24, nameof(PrivilegedAccessGranted));
public static readonly AuditEventType PrivilegedAccessRevoked = new(25, nameof(PrivilegedAccessRevoked));
// Organization events
public static readonly AuditEventType OrganizationCreated = new(30, nameof(OrganizationCreated));
public static readonly AuditEventType OrganizationUpdated = new(31, nameof(OrganizationUpdated));
public static readonly AuditEventType GroupMemberAdded = new(32, nameof(GroupMemberAdded));
public static readonly AuditEventType GroupMemberRemoved = new(33, nameof(GroupMemberRemoved));
// Policy events
public static readonly AuditEventType PolicyCreated = new(40, nameof(PolicyCreated));
public static readonly AuditEventType PolicyActivated = new(41, nameof(PolicyActivated));
public static readonly AuditEventType PolicyDeactivated = new(42, nameof(PolicyDeactivated));
// Compliance events
public static readonly AuditEventType ComplianceReportGenerated = new(50, nameof(ComplianceReportGenerated));
public static readonly AuditEventType ViolationDetected = new(51, nameof(ViolationDetected));
public static readonly AuditEventType ViolationResolved = new(52, nameof(ViolationResolved));
public AuditEventType(int id, string name) : base(id, name) { }
public static AuditEventType? FromId(int id) => GetAll<AuditEventType>().FirstOrDefault(e => e.Id == id);
public static AuditEventType? FromName(string name) => GetAll<AuditEventType>().FirstOrDefault(e => e.Name == name);
}

View File

@@ -0,0 +1,101 @@
using IamService.Domain.SeedWork;
namespace IamService.Domain.AggregatesModel.AuditAggregate;
/// <summary>
/// EN: Audit log aggregate root for system-wide event logging.
/// VI: Aggregate root audit log cho ghi nhận sự kiện toàn hệ thống.
/// </summary>
public class AuditLog : Entity, IAggregateRoot
{
private AuditEventType _eventType = null!;
private Guid? _actorId;
private string? _actorEmail;
private string _resourceType = null!;
private Guid? _resourceId;
private string? _action;
private string? _details;
private string? _ipAddress;
private string? _userAgent;
private bool _success;
private DateTime _timestamp;
#region Properties
public AuditEventType EventType => _eventType;
public Guid? ActorId => _actorId;
public string? ActorEmail => _actorEmail;
public string ResourceType => _resourceType;
public Guid? ResourceId => _resourceId;
public string? Action => _action;
public string? Details => _details;
public string? IpAddress => _ipAddress;
public string? UserAgent => _userAgent;
public bool Success => _success;
public DateTime Timestamp => _timestamp;
#endregion
protected AuditLog() { }
private AuditLog(
AuditEventType eventType,
Guid? actorId,
string? actorEmail,
string resourceType,
Guid? resourceId,
string? action,
string? details,
string? ipAddress,
string? userAgent,
bool success)
{
Id = Guid.NewGuid();
_eventType = eventType;
_actorId = actorId;
_actorEmail = actorEmail;
_resourceType = resourceType;
_resourceId = resourceId;
_action = action;
_details = details;
_ipAddress = ipAddress;
_userAgent = userAgent;
_success = success;
_timestamp = DateTime.UtcNow;
}
public static AuditLog Create(
AuditEventType eventType,
string resourceType,
Guid? actorId = null,
string? actorEmail = null,
Guid? resourceId = null,
string? action = null,
string? details = null,
string? ipAddress = null,
string? userAgent = null,
bool success = true)
{
if (string.IsNullOrWhiteSpace(resourceType))
throw new ArgumentException("Resource type is required", nameof(resourceType));
return new AuditLog(eventType, actorId, actorEmail, resourceType, resourceId, action, details, ipAddress, userAgent, success);
}
/// <summary>
/// EN: Create login event.
/// VI: Tạo event đăng nhập.
/// </summary>
public static AuditLog LoginEvent(Guid userId, string email, string? ipAddress, string? userAgent, bool success = true)
=> Create(
success ? AuditEventType.Login : AuditEventType.LoginFailed,
"User", userId, email, userId,
success ? "User logged in" : "Login attempt failed",
null, ipAddress, userAgent, success);
/// <summary>
/// EN: Create access granted event.
/// VI: Tạo event cấp quyền truy cập.
/// </summary>
public static AuditLog AccessGrantedEvent(Guid actorId, string actorEmail, string resourceType, Guid resourceId, string permission)
=> Create(AuditEventType.AccessGranted, resourceType, actorId, actorEmail, resourceId,
$"Access granted: {permission}", null, null, null);
}

View File

@@ -0,0 +1,20 @@
using IamService.Domain.SeedWork;
namespace IamService.Domain.AggregatesModel.AuditAggregate;
/// <summary>
/// EN: Repository interface for AuditLog aggregate.
/// VI: Interface repository cho AuditLog aggregate.
/// </summary>
public interface IAuditLogRepository : IRepository<AuditLog>
{
AuditLog Add(AuditLog log);
Task<IEnumerable<AuditLog>> GetByActorIdAsync(Guid actorId, int take = 100, CancellationToken cancellationToken = default);
Task<IEnumerable<AuditLog>> GetByResourceAsync(string resourceType, Guid resourceId, int take = 100, CancellationToken cancellationToken = default);
Task<IEnumerable<AuditLog>> SearchAsync(
DateTime? fromDate, DateTime? toDate,
int? eventTypeId = null, Guid? actorId = null, string? resourceType = null,
int skip = 0, int take = 50,
CancellationToken cancellationToken = default);
Task<long> GetCountAsync(DateTime? fromDate, DateTime? toDate, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,94 @@
using IamService.Domain.Events;
using IamService.Domain.SeedWork;
namespace IamService.Domain.AggregatesModel.ComplianceAggregate;
/// <summary>
/// EN: Compliance report aggregate root.
/// VI: Aggregate root báo cáo tuân thủ.
/// </summary>
public class ComplianceReport : Entity, IAggregateRoot
{
private string _name = null!;
private ComplianceReportType _reportType = null!;
private ComplianceReportStatus _status = null!;
private Guid _generatedByUserId;
private DateTime _createdAt;
private DateTime? _completedAt;
private string? _summary;
private int _totalChecks;
private int _passedChecks;
private int _failedChecks;
private readonly List<ComplianceViolation> _violations = [];
#region Properties
public string Name => _name;
public ComplianceReportType ReportType => _reportType;
public ComplianceReportStatus Status => _status;
public Guid GeneratedByUserId => _generatedByUserId;
public DateTime CreatedAt => _createdAt;
public DateTime? CompletedAt => _completedAt;
public string? Summary => _summary;
public int TotalChecks => _totalChecks;
public int PassedChecks => _passedChecks;
public int FailedChecks => _failedChecks;
public IReadOnlyCollection<ComplianceViolation> Violations => _violations.AsReadOnly();
public double CompliancePercentage => _totalChecks > 0 ? (double)_passedChecks / _totalChecks * 100 : 0;
#endregion
protected ComplianceReport() { }
private ComplianceReport(string name, ComplianceReportType reportType, Guid generatedByUserId)
{
Id = Guid.NewGuid();
_name = name;
_reportType = reportType;
_status = ComplianceReportStatus.Pending;
_generatedByUserId = generatedByUserId;
_createdAt = DateTime.UtcNow;
AddDomainEvent(new ComplianceReportCreatedEvent(Id, name, reportType.Name));
}
public static ComplianceReport Create(string name, ComplianceReportType reportType, Guid generatedByUserId)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Name is required", nameof(name));
return new ComplianceReport(name, reportType, generatedByUserId);
}
public void StartGenerating()
{
if (_status != ComplianceReportStatus.Pending)
throw new InvalidOperationException("Only pending reports can be started");
_status = ComplianceReportStatus.Generating;
}
public void SetResults(int totalChecks, int passedChecks, string? summary = null)
{
_totalChecks = totalChecks;
_passedChecks = passedChecks;
_failedChecks = totalChecks - passedChecks;
_summary = summary;
}
public void AddViolation(string rule, ViolationSeverity severity, string description, string? remediation = null)
{
var violation = new ComplianceViolation(Id, rule, severity, description, remediation);
_violations.Add(violation);
}
public void Complete()
{
_status = ComplianceReportStatus.Completed;
_completedAt = DateTime.UtcNow;
AddDomainEvent(new ComplianceReportCompletedEvent(Id, _passedChecks, _failedChecks, _violations.Count));
}
public void Fail(string? reason = null)
{
_status = ComplianceReportStatus.Failed;
_summary = reason ?? "Report generation failed";
}
}

View File

@@ -0,0 +1,62 @@
using IamService.Domain.SeedWork;
namespace IamService.Domain.AggregatesModel.ComplianceAggregate;
/// <summary>
/// EN: Compliance report type enumeration.
/// VI: Enumeration loại báo cáo tuân thủ.
/// </summary>
public class ComplianceReportType : Enumeration
{
public static readonly ComplianceReportType GDPR = new(1, nameof(GDPR));
public static readonly ComplianceReportType SOC2 = new(2, nameof(SOC2));
public static readonly ComplianceReportType ISO27001 = new(3, nameof(ISO27001));
public static readonly ComplianceReportType HIPAA = new(4, nameof(HIPAA));
public static readonly ComplianceReportType AccessReview = new(5, nameof(AccessReview));
public ComplianceReportType(int id, string name) : base(id, name) { }
public static ComplianceReportType? FromId(int id) => id switch
{
1 => GDPR,
2 => SOC2,
3 => ISO27001,
4 => HIPAA,
5 => AccessReview,
_ => null
};
}
/// <summary>
/// EN: Compliance report status enumeration.
/// VI: Enumeration trạng thái báo cáo.
/// </summary>
public class ComplianceReportStatus : Enumeration
{
public static readonly ComplianceReportStatus Pending = new(1, nameof(Pending));
public static readonly ComplianceReportStatus Generating = new(2, nameof(Generating));
public static readonly ComplianceReportStatus Completed = new(3, nameof(Completed));
public static readonly ComplianceReportStatus Failed = new(4, nameof(Failed));
public ComplianceReportStatus(int id, string name) : base(id, name) { }
public static ComplianceReportStatus? FromId(int id) => id switch
{
1 => Pending, 2 => Generating, 3 => Completed, 4 => Failed, _ => null
};
}
/// <summary>
/// EN: Violation severity enumeration.
/// VI: Enumeration mức độ nghiêm trọng vi phạm.
/// </summary>
public class ViolationSeverity : Enumeration
{
public static readonly ViolationSeverity Low = new(1, nameof(Low));
public static readonly ViolationSeverity Medium = new(2, nameof(Medium));
public static readonly ViolationSeverity High = new(3, nameof(High));
public static readonly ViolationSeverity Critical = new(4, nameof(Critical));
public ViolationSeverity(int id, string name) : base(id, name) { }
public static ViolationSeverity? FromId(int id) => id switch { 1 => Low, 2 => Medium, 3 => High, 4 => Critical, _ => null };
}

View File

@@ -0,0 +1,48 @@
using IamService.Domain.SeedWork;
namespace IamService.Domain.AggregatesModel.ComplianceAggregate;
/// <summary>
/// EN: Compliance violation entity.
/// VI: Entity vi phạm tuân thủ.
/// </summary>
public class ComplianceViolation : Entity
{
private Guid _reportId;
private string _rule = null!;
private ViolationSeverity _severity = null!;
private string _description = null!;
private string? _remediation;
private string? _affectedResource;
private bool _resolved;
private DateTime? _resolvedAt;
public Guid ReportId => _reportId;
public string Rule => _rule;
public ViolationSeverity Severity => _severity;
public string Description => _description;
public string? Remediation => _remediation;
public string? AffectedResource => _affectedResource;
public bool Resolved => _resolved;
public DateTime? ResolvedAt => _resolvedAt;
protected ComplianceViolation() { _severity = ViolationSeverity.Medium; }
public ComplianceViolation(Guid reportId, string rule, ViolationSeverity severity, string description, string? remediation = null, string? affectedResource = null)
{
Id = Guid.NewGuid();
_reportId = reportId;
_rule = rule;
_severity = severity;
_description = description;
_remediation = remediation;
_affectedResource = affectedResource;
_resolved = false;
}
public void Resolve()
{
_resolved = true;
_resolvedAt = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,18 @@
using IamService.Domain.SeedWork;
namespace IamService.Domain.AggregatesModel.ComplianceAggregate;
/// <summary>
/// EN: Repository interface for ComplianceReport aggregate.
/// VI: Interface repository cho ComplianceReport aggregate.
/// </summary>
public interface IComplianceReportRepository : IRepository<ComplianceReport>
{
ComplianceReport Add(ComplianceReport report);
void Update(ComplianceReport report);
Task<ComplianceReport?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<ComplianceReport?> GetByIdWithViolationsAsync(Guid id, CancellationToken cancellationToken = default);
Task<IEnumerable<ComplianceReport>> GetByTypeAsync(int reportTypeId, int take = 20, CancellationToken cancellationToken = default);
Task<IEnumerable<ComplianceReport>> GetRecentAsync(int take = 20, CancellationToken cancellationToken = default);
Task<IEnumerable<ComplianceViolation>> GetUnresolvedViolationsAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,21 @@
using IamService.Domain.SeedWork;
namespace IamService.Domain.Events;
/// <summary>
/// EN: Event when compliance report is created.
/// VI: Event khi báo cáo tuân thủ được tạo.
/// </summary>
public record ComplianceReportCreatedEvent(Guid ReportId, string Name, string ReportType) : IDomainEvent
{
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
/// <summary>
/// EN: Event when compliance report is completed.
/// VI: Event khi báo cáo tuân thủ hoàn thành.
/// </summary>
public record ComplianceReportCompletedEvent(Guid ReportId, int PassedChecks, int FailedChecks, int ViolationCount) : IDomainEvent
{
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}

View File

@@ -13,6 +13,8 @@ using IamService.Domain.AggregatesModel.VerificationAggregate;
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
using IamService.Domain.AggregatesModel.AccessReviewAggregate;
using IamService.Domain.AggregatesModel.PrivilegedAccessAggregate;
using IamService.Domain.AggregatesModel.AuditAggregate;
using IamService.Domain.AggregatesModel.ComplianceAggregate;
using IamService.Domain.SeedWork;
using IamService.Infrastructure.Email;
using IamService.Infrastructure.IdentityServer;
@@ -159,6 +161,8 @@ public static class DependencyInjection
services.AddScoped<IAccessRequestRepository, AccessRequestRepository>();
services.AddScoped<IAccessReviewRepository, AccessReviewRepository>();
services.AddScoped<IPrivilegedAccessRepository, PrivilegedAccessRepository>();
services.AddScoped<IAuditLogRepository, AuditLogRepository>();
services.AddScoped<IComplianceReportRepository, ComplianceReportRepository>();
services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<IamServiceContext>());
// EN: Configure Redis caching (skip in Testing environment)

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using IamService.Domain.AggregatesModel.AuditAggregate;
namespace IamService.Infrastructure.EntityConfigurations;
/// <summary>
/// EN: Entity configuration for AuditLog.
/// VI: Cấu hình entity cho AuditLog.
/// </summary>
public class AuditLogEntityConfiguration : IEntityTypeConfiguration<AuditLog>
{
public void Configure(EntityTypeBuilder<AuditLog> builder)
{
builder.ToTable("AuditLogs");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedNever();
builder.Property<Guid?>("_actorId").HasColumnName("ActorId");
builder.Property<string?>("_actorEmail").HasColumnName("ActorEmail").HasMaxLength(256);
builder.Property<string>("_resourceType").HasColumnName("ResourceType").HasMaxLength(100).IsRequired();
builder.Property<Guid?>("_resourceId").HasColumnName("ResourceId");
builder.Property<string?>("_action").HasColumnName("Action").HasMaxLength(200);
builder.Property<string?>("_details").HasColumnName("Details").HasMaxLength(4000);
builder.Property<string?>("_ipAddress").HasColumnName("IpAddress").HasMaxLength(45);
builder.Property<string?>("_userAgent").HasColumnName("UserAgent").HasMaxLength(500);
builder.Property<bool>("_success").HasColumnName("Success");
builder.Property<DateTime>("_timestamp").HasColumnName("Timestamp").IsRequired();
builder.Property<AuditEventType>("_eventType")
.HasColumnName("EventTypeId")
.HasConversion(v => v.Id, v => AuditEventType.FromId(v) ?? AuditEventType.Login);
builder.Ignore(x => x.DomainEvents);
// Indexes for common queries
builder.HasIndex("_timestamp").HasDatabaseName("IX_AuditLogs_Timestamp");
builder.HasIndex("_actorId").HasDatabaseName("IX_AuditLogs_ActorId");
builder.HasIndex("_eventType").HasDatabaseName("IX_AuditLogs_EventType");
builder.HasIndex("_resourceType", "_resourceId").HasDatabaseName("IX_AuditLogs_Resource");
}
}

View File

@@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using IamService.Domain.AggregatesModel.ComplianceAggregate;
namespace IamService.Infrastructure.EntityConfigurations;
public class ComplianceReportEntityConfiguration : IEntityTypeConfiguration<ComplianceReport>
{
public void Configure(EntityTypeBuilder<ComplianceReport> builder)
{
builder.ToTable("ComplianceReports");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedNever();
builder.Property<string>("_name").HasColumnName("Name").HasMaxLength(200).IsRequired();
builder.Property<Guid>("_generatedByUserId").HasColumnName("GeneratedByUserId").IsRequired();
builder.Property<DateTime>("_createdAt").HasColumnName("CreatedAt").IsRequired();
builder.Property<DateTime?>("_completedAt").HasColumnName("CompletedAt");
builder.Property<string?>("_summary").HasColumnName("Summary").HasMaxLength(4000);
builder.Property<int>("_totalChecks").HasColumnName("TotalChecks");
builder.Property<int>("_passedChecks").HasColumnName("PassedChecks");
builder.Property<int>("_failedChecks").HasColumnName("FailedChecks");
builder.Property<ComplianceReportType>("_reportType")
.HasColumnName("ReportTypeId")
.HasConversion(v => v.Id, v => ComplianceReportType.FromId(v) ?? ComplianceReportType.GDPR);
builder.Property<ComplianceReportStatus>("_status")
.HasColumnName("StatusId")
.HasConversion(v => v.Id, v => ComplianceReportStatus.FromId(v) ?? ComplianceReportStatus.Pending);
builder.HasMany(x => x.Violations).WithOne().HasForeignKey("_reportId").OnDelete(DeleteBehavior.Cascade);
builder.Ignore(x => x.DomainEvents);
builder.HasIndex("_createdAt").HasDatabaseName("IX_ComplianceReports_CreatedAt");
}
}
public class ComplianceViolationEntityConfiguration : IEntityTypeConfiguration<ComplianceViolation>
{
public void Configure(EntityTypeBuilder<ComplianceViolation> builder)
{
builder.ToTable("ComplianceViolations");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedNever();
builder.Property<Guid>("_reportId").HasColumnName("ReportId").IsRequired();
builder.Property<string>("_rule").HasColumnName("Rule").HasMaxLength(200).IsRequired();
builder.Property<string>("_description").HasColumnName("Description").HasMaxLength(1000).IsRequired();
builder.Property<string?>("_remediation").HasColumnName("Remediation").HasMaxLength(1000);
builder.Property<string?>("_affectedResource").HasColumnName("AffectedResource").HasMaxLength(200);
builder.Property<bool>("_resolved").HasColumnName("Resolved");
builder.Property<DateTime?>("_resolvedAt").HasColumnName("ResolvedAt");
builder.Property<ViolationSeverity>("_severity")
.HasColumnName("SeverityId")
.HasConversion(v => v.Id, v => ViolationSeverity.FromId(v) ?? ViolationSeverity.Medium);
builder.Ignore(x => x.DomainEvents);
}
}

View File

@@ -11,6 +11,8 @@ using IamService.Domain.AggregatesModel.VerificationAggregate;
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
using IamService.Domain.AggregatesModel.AccessReviewAggregate;
using IamService.Domain.AggregatesModel.PrivilegedAccessAggregate;
using IamService.Domain.AggregatesModel.AuditAggregate;
using IamService.Domain.AggregatesModel.ComplianceAggregate;
using IamService.Domain.SeedWork;
namespace IamService.Infrastructure;
@@ -145,6 +147,24 @@ public class IamServiceContext : IdentityDbContext<ApplicationUser, ApplicationR
/// </summary>
public DbSet<PrivilegedAccessGrant> PrivilegedAccessGrants { get; set; } = null!;
/// <summary>
/// EN: Audit logs table.
/// VI: Bảng audit logs.
/// </summary>
public DbSet<AuditLog> AuditLogs { get; set; } = null!;
/// <summary>
/// EN: Compliance reports table.
/// VI: Bảng báo cáo tuân thủ.
/// </summary>
public DbSet<ComplianceReport> ComplianceReports { get; set; } = null!;
/// <summary>
/// EN: Compliance violations table.
/// VI: Bảng vi phạm tuân thủ.
/// </summary>
public DbSet<ComplianceViolation> ComplianceViolations { 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.

View File

@@ -0,0 +1,64 @@
using Microsoft.EntityFrameworkCore;
using IamService.Domain.AggregatesModel.AuditAggregate;
using IamService.Domain.SeedWork;
namespace IamService.Infrastructure.Repositories;
public class AuditLogRepository : IAuditLogRepository
{
private readonly IamServiceContext _context;
public IUnitOfWork UnitOfWork => _context;
public AuditLogRepository(IamServiceContext context) => _context = context;
public AuditLog Add(AuditLog log) => _context.AuditLogs.Add(log).Entity;
public async Task<IEnumerable<AuditLog>> GetByActorIdAsync(Guid actorId, int take = 100, CancellationToken cancellationToken = default)
=> await _context.AuditLogs
.Where(x => EF.Property<Guid?>(x, "_actorId") == actorId)
.OrderByDescending(x => EF.Property<DateTime>(x, "_timestamp"))
.Take(take)
.ToListAsync(cancellationToken);
public async Task<IEnumerable<AuditLog>> GetByResourceAsync(string resourceType, Guid resourceId, int take = 100, CancellationToken cancellationToken = default)
=> await _context.AuditLogs
.Where(x => EF.Property<string>(x, "_resourceType") == resourceType && EF.Property<Guid?>(x, "_resourceId") == resourceId)
.OrderByDescending(x => EF.Property<DateTime>(x, "_timestamp"))
.Take(take)
.ToListAsync(cancellationToken);
public async Task<IEnumerable<AuditLog>> SearchAsync(
DateTime? fromDate, DateTime? toDate,
int? eventTypeId = null, Guid? actorId = null, string? resourceType = null,
int skip = 0, int take = 50,
CancellationToken cancellationToken = default)
{
var query = _context.AuditLogs.AsQueryable();
if (fromDate.HasValue)
query = query.Where(x => EF.Property<DateTime>(x, "_timestamp") >= fromDate.Value);
if (toDate.HasValue)
query = query.Where(x => EF.Property<DateTime>(x, "_timestamp") <= toDate.Value);
if (eventTypeId.HasValue)
query = query.Where(x => EF.Property<AuditEventType>(x, "_eventType").Id == eventTypeId.Value);
if (actorId.HasValue)
query = query.Where(x => EF.Property<Guid?>(x, "_actorId") == actorId.Value);
if (!string.IsNullOrEmpty(resourceType))
query = query.Where(x => EF.Property<string>(x, "_resourceType") == resourceType);
return await query
.OrderByDescending(x => EF.Property<DateTime>(x, "_timestamp"))
.Skip(skip).Take(take)
.ToListAsync(cancellationToken);
}
public async Task<long> GetCountAsync(DateTime? fromDate, DateTime? toDate, CancellationToken cancellationToken = default)
{
var query = _context.AuditLogs.AsQueryable();
if (fromDate.HasValue)
query = query.Where(x => EF.Property<DateTime>(x, "_timestamp") >= fromDate.Value);
if (toDate.HasValue)
query = query.Where(x => EF.Property<DateTime>(x, "_timestamp") <= toDate.Value);
return await query.LongCountAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore;
using IamService.Domain.AggregatesModel.ComplianceAggregate;
using IamService.Domain.SeedWork;
namespace IamService.Infrastructure.Repositories;
public class ComplianceReportRepository : IComplianceReportRepository
{
private readonly IamServiceContext _context;
public IUnitOfWork UnitOfWork => _context;
public ComplianceReportRepository(IamServiceContext context) => _context = context;
public ComplianceReport Add(ComplianceReport report) => _context.ComplianceReports.Add(report).Entity;
public void Update(ComplianceReport report) => _context.Entry(report).State = EntityState.Modified;
public async Task<ComplianceReport?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
=> await _context.ComplianceReports.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
public async Task<ComplianceReport?> GetByIdWithViolationsAsync(Guid id, CancellationToken cancellationToken = default)
=> await _context.ComplianceReports.Include(x => x.Violations).FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
public async Task<IEnumerable<ComplianceReport>> GetByTypeAsync(int reportTypeId, int take = 20, CancellationToken cancellationToken = default)
=> await _context.ComplianceReports
.Where(x => EF.Property<ComplianceReportType>(x, "_reportType").Id == reportTypeId)
.OrderByDescending(x => EF.Property<DateTime>(x, "_createdAt"))
.Take(take)
.ToListAsync(cancellationToken);
public async Task<IEnumerable<ComplianceReport>> GetRecentAsync(int take = 20, CancellationToken cancellationToken = default)
=> await _context.ComplianceReports
.OrderByDescending(x => EF.Property<DateTime>(x, "_createdAt"))
.Take(take)
.ToListAsync(cancellationToken);
public async Task<IEnumerable<ComplianceViolation>> GetUnresolvedViolationsAsync(CancellationToken cancellationToken = default)
=> await _context.ComplianceViolations
.Where(x => !EF.Property<bool>(x, "_resolved"))
.ToListAsync(cancellationToken);
}