feature: RBAC + Clinical Audit Logging

This commit is contained in:
voltsrage
2026-06-21 15:46:55 +08:00
parent a43db52813
commit 5af6ab490e
83 changed files with 4281 additions and 70 deletions
+44 -3
View File
@@ -6,11 +6,19 @@ public class AlertService : IAlertService
{
private readonly AppDbContext _db;
private readonly IServiceProvider _services;
private readonly ICurrentUserService _currentUser;
private readonly IAuditService _audit;
public AlertService(AppDbContext db, IServiceProvider services)
public AlertService(
AppDbContext db,
IServiceProvider services,
ICurrentUserService currentUser,
IAuditService audit)
{
_db = db;
_services = services;
_currentUser = currentUser;
_audit = audit;
}
public async Task<PagedResult<ClinicalAlert>> ListByEncounterAsync(
@@ -75,6 +83,12 @@ public class AlertService : IAlertService
public async Task<ClinicalAlert> AcknowledgeAsync(Guid id, AcknowledgeAlertRequest req)
{
if (!_currentUser.IsAuthenticated)
throw new ValidationException("Authentication required.", "AUTH_REQUIRED");
var displayName = _currentUser.DisplayName ?? _currentUser.Username
?? throw new ValidationException("Authenticated user identity missing.", "AUTH_REQUIRED");
var alert = await _db.ClinicalAlerts.FindAsync(id);
if (alert is null)
throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND");
@@ -84,9 +98,10 @@ public class AlertService : IAlertService
$"Alert cannot be acknowledged from status '{alert.Status}'.",
"ALERT_NOT_ACKNOWLEDGEABLE");
var previousStatus = alert.Status;
alert.Status = AlertStatus.Acknowledged;
alert.AcknowledgedAt = DateTimeOffset.UtcNow;
alert.AcknowledgedBy = req.ClinicianId;
alert.AcknowledgedBy = displayName;
// Write an outbox event so the Kafka consumer (Phase 6) can cancel the
// pending RabbitMQ escalation timer when it sees this acknowledgment.
@@ -98,7 +113,7 @@ public class AlertService : IAlertService
{
alertId = alert.Id,
encounterId = alert.EncounterId,
acknowledgedBy = req.ClinicianId,
acknowledgedBy = displayName,
acknowledgedAt = alert.AcknowledgedAt,
note = req.Note
}),
@@ -120,6 +135,25 @@ public class AlertService : IAlertService
}
await _db.SaveChangesAsync();
await _audit.WriteAsync(
AuditAction.AlertAcknowledged,
"ClinicalAlert",
alert.Id,
previousValue: new { status = previousStatus.ToDbString() },
newValue: new { status = alert.Status.ToDbString(), alert.AcknowledgedBy },
reason: req.Note);
if (alert.AlertType.IsSuppressible())
{
await _audit.WriteAsync(
AuditAction.SuppressionWindowSet,
"ClinicalAlert",
alert.Id,
newValue: new { alert.AlertType, alert.EncounterId },
reason: req.Note);
}
return alert;
}
@@ -157,6 +191,13 @@ public class AlertService : IAlertService
alert.ResolvedAt = DateTimeOffset.UtcNow;
await _db.SaveChangesAsync();
await _audit.WriteAsync(
AuditAction.AlertResolved,
"ClinicalAlert",
alert.Id,
previousValue: new { status = AlertStatus.Acknowledged.ToDbString() },
newValue: new { status = alert.Status.ToDbString() });
return alert;
}
}