f20d19cf4b
- Register IAuthAuditSql (AuthAuditSql) in DI - Add AuthAuditMiddleware to pipeline after authentication - Fix AuthAuditSql using statement + NpgsqlInet construction - Fix GetAuditLogsEndpoint response mapping (AuthAuditLogEntry → AuditLogItem) - Build verified (Release mode, 0 errors) Implements audit trail for all /api/auth/* endpoints: - Captures event type, status, IP, user agent, endpoint, error details - Immutable append-only storage with compliance views - Async non-blocking logging with graceful failure handling - Ready for 0047 migration application and testing WBS: AEG-X-005 (JWT/OIDC/fail-closed) + AEG-AUTH-001 (Audit) Gate: G3 (Shadow Run API) Status: CODE_COMPLETE → MIGRATION_READY Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
109 lines
3.4 KiB
C#
109 lines
3.4 KiB
C#
using FastEndpoints;
|
|
using KArtSell.Host.Features.Audit;
|
|
|
|
namespace KArtSell.Host.Endpoints.Audit;
|
|
|
|
/// <summary>
|
|
/// GET /api/admin/audit-logs - Retrieve authentication audit logs
|
|
/// Requires Admin or SecurityOfficer role
|
|
/// </summary>
|
|
public sealed class GetAuditLogsEndpoint : Endpoint<GetAuditLogsRequest, GetAuditLogsResponse>
|
|
{
|
|
private readonly IAuthAuditSql _auditSql;
|
|
|
|
public GetAuditLogsEndpoint(IAuthAuditSql auditSql)
|
|
{
|
|
_auditSql = auditSql;
|
|
}
|
|
|
|
public override void Configure()
|
|
{
|
|
Get("/audit-logs");
|
|
Roles("Admin", "SecurityOfficer");
|
|
Description(x => x
|
|
.WithName("Get Audit Logs")
|
|
.WithDescription("Retrieve authentication audit logs for compliance reporting")
|
|
.Accepts<GetAuditLogsRequest>("application/json")
|
|
.Produces<GetAuditLogsResponse>(200, "application/json"));
|
|
}
|
|
|
|
public override async Task HandleAsync(GetAuditLogsRequest req, CancellationToken ct)
|
|
{
|
|
var logs = await _auditSql.GetAuditLogsAsync(
|
|
startDate: req.StartDate,
|
|
endDate: req.EndDate,
|
|
eventType: req.EventType,
|
|
username: req.Username,
|
|
limit: req.PageSize > 1000 ? 1000 : req.PageSize, // Cap at 1000
|
|
offset: (req.Page - 1) * req.PageSize,
|
|
ct: ct
|
|
);
|
|
|
|
var total = await _auditSql.GetAuditLogsCountAsync(
|
|
startDate: req.StartDate,
|
|
endDate: req.EndDate,
|
|
eventType: req.EventType,
|
|
username: req.Username,
|
|
ct: ct
|
|
);
|
|
|
|
var items = logs.Select(entry => new AuditLogItem
|
|
{
|
|
AuditId = entry.AuditId,
|
|
EventType = entry.EventType,
|
|
IdentityId = entry.IdentityId,
|
|
Username = entry.Username,
|
|
Role = entry.Role,
|
|
IpAddress = entry.IpAddress,
|
|
Endpoint = entry.Endpoint,
|
|
HttpMethod = entry.HttpMethod,
|
|
Status = entry.Status,
|
|
ErrorCode = entry.ErrorCode,
|
|
ErrorMessage = entry.ErrorMessage,
|
|
OccurredAt = entry.OccurredAt
|
|
}).ToList();
|
|
|
|
await Send.OkAsync(new GetAuditLogsResponse
|
|
{
|
|
Items = items,
|
|
Total = total,
|
|
Page = req.Page,
|
|
PageSize = req.PageSize
|
|
}, ct);
|
|
}
|
|
}
|
|
|
|
public class GetAuditLogsRequest
|
|
{
|
|
public DateTime? StartDate { get; set; }
|
|
public DateTime? EndDate { get; set; }
|
|
public string? EventType { get; set; } // LOGIN, LOGOUT, MFA_SETUP, etc.
|
|
public string? Username { get; set; }
|
|
public int Page { get; set; } = 1;
|
|
public int PageSize { get; set; } = 50;
|
|
}
|
|
|
|
public class GetAuditLogsResponse
|
|
{
|
|
public required List<AuditLogItem> Items { get; set; }
|
|
public int Total { get; set; }
|
|
public int Page { get; set; }
|
|
public int PageSize { get; set; }
|
|
}
|
|
|
|
public class AuditLogItem
|
|
{
|
|
public Guid AuditId { get; set; }
|
|
public required string EventType { get; set; }
|
|
public Guid? IdentityId { get; set; }
|
|
public string? Username { get; set; }
|
|
public string? Role { get; set; }
|
|
public string? IpAddress { get; set; }
|
|
public string? Endpoint { get; set; }
|
|
public string? HttpMethod { get; set; }
|
|
public required string Status { get; set; }
|
|
public string? ErrorCode { get; set; }
|
|
public string? ErrorMessage { get; set; }
|
|
public DateTime OccurredAt { get; set; }
|
|
}
|