feature: RBAC + Clinical Audit Logging
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
using Hl7.Fhir.Model;
|
||||
using Hl7.Fhir.Serialization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
/// <summary>
|
||||
/// FHIR R4 inbound facade: single-resource create and transaction Bundle processing.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("fhir/R4")]
|
||||
[AuthorizePermission(ClinicalPermissions.FhirIngest)]
|
||||
[ServiceFilter(typeof(FhirExceptionFilter))]
|
||||
public class FhirIngestController : ControllerBase
|
||||
{
|
||||
@@ -52,9 +57,18 @@ public class FhirIngestController : ControllerBase
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates a Patient from a FHIR R4 Patient resource (idempotent by identifier).
|
||||
/// </summary>
|
||||
/// <returns>The persisted Patient resource with Location header.</returns>
|
||||
[HttpPost("Patient")]
|
||||
[Consumes("application/fhir+json")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Hl7.Fhir.Model.Patient), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> CreatePatient()
|
||||
{
|
||||
var fhir = await ParseBodyAsync<Hl7.Fhir.Model.Patient>();
|
||||
@@ -64,12 +78,22 @@ public class FhirIngestController : ControllerBase
|
||||
ExternalResourceType.Patient, patient.Id, _options.PatientIdentifierSystems);
|
||||
var response = _patientMapper.ToFhirResponse(patient, hospitalId);
|
||||
_metrics.FhirIngestTotal.WithLabels("Patient", "success").Inc();
|
||||
return Created($"{Request.Path}/{patient.Id}", Serialize(response));
|
||||
Response.Headers.Location = $"{Request.Path}/{patient.Id}";
|
||||
return Serialize(response, StatusCodes.Status201Created);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates an Encounter from a FHIR R4 Encounter resource (idempotent by identifier).
|
||||
/// </summary>
|
||||
/// <returns>The persisted Encounter resource with Location header.</returns>
|
||||
[HttpPost("Encounter")]
|
||||
[Consumes("application/fhir+json")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Hl7.Fhir.Model.Encounter), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> CreateEncounter()
|
||||
{
|
||||
var fhir = await ParseBodyAsync<Hl7.Fhir.Model.Encounter>();
|
||||
@@ -79,12 +103,22 @@ public class FhirIngestController : ControllerBase
|
||||
ExternalResourceType.Encounter, encounter.Id, _options.EncounterIdentifierSystems);
|
||||
var response = _encounterMapper.ToFhirResponse(encounter, hospitalId);
|
||||
_metrics.FhirIngestTotal.WithLabels("Encounter", "success").Inc();
|
||||
return Created($"{Request.Path}/{encounter.Id}", Serialize(response));
|
||||
Response.Headers.Location = $"{Request.Path}/{encounter.Id}";
|
||||
return Serialize(response, StatusCodes.Status201Created);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ingests one or more observations from a FHIR R4 Observation resource.
|
||||
/// </summary>
|
||||
/// <returns>The last persisted Observation resource with Location header.</returns>
|
||||
[HttpPost("Observation")]
|
||||
[Consumes("application/fhir+json")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Hl7.Fhir.Model.Observation), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> CreateObservation()
|
||||
{
|
||||
var fhir = await ParseBodyAsync<Hl7.Fhir.Model.Observation>();
|
||||
@@ -104,12 +138,22 @@ public class FhirIngestController : ControllerBase
|
||||
}
|
||||
|
||||
_metrics.FhirIngestTotal.WithLabels("Observation", "success").Inc();
|
||||
return Created(Request.Path.Value!, Serialize(lastResponse!));
|
||||
Response.Headers.Location = Request.Path.Value!;
|
||||
return Serialize(lastResponse!, StatusCodes.Status201Created);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a medication administration from a FHIR R4 MedicationAdministration resource.
|
||||
/// </summary>
|
||||
/// <returns>The persisted MedicationAdministration resource with Location header.</returns>
|
||||
[HttpPost("MedicationAdministration")]
|
||||
[Consumes("application/fhir+json")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Hl7.Fhir.Model.MedicationAdministration), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> CreateMedicationAdministration()
|
||||
{
|
||||
var fhir = await ParseBodyAsync<Hl7.Fhir.Model.MedicationAdministration>();
|
||||
@@ -117,13 +161,22 @@ public class FhirIngestController : ControllerBase
|
||||
var med = await _medications.CreateAsync(encounterId, req);
|
||||
var response = new Hl7.Fhir.Model.MedicationAdministration { Id = med.Id.ToString() };
|
||||
_metrics.FhirIngestTotal.WithLabels("MedicationAdministration", "success").Inc();
|
||||
return Created($"{Request.Path}/{med.Id}", Serialize(response));
|
||||
Response.Headers.Location = $"{Request.Path}/{med.Id}";
|
||||
return Serialize(response, StatusCodes.Status201Created);
|
||||
}
|
||||
|
||||
/// <summary>Accepts Bundle.type=transaction (ADT admit) or batch.</summary>
|
||||
/// <summary>
|
||||
/// Processes a FHIR R4 transaction Bundle (e.g. ADT admit with Patient + Encounter).
|
||||
/// </summary>
|
||||
/// <returns>A transaction-response Bundle with per-entry outcomes.</returns>
|
||||
[HttpPost]
|
||||
[Consumes("application/fhir+json")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Bundle), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ProcessBundle()
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
@@ -135,7 +188,7 @@ public class FhirIngestController : ControllerBase
|
||||
|
||||
var responseBundle = await _bundleProcessor.ProcessTransactionAsync(bundle);
|
||||
_metrics.FhirIngestTotal.WithLabels("Bundle", "success").Inc();
|
||||
return Ok(Serialize(responseBundle));
|
||||
return Serialize(responseBundle);
|
||||
}
|
||||
|
||||
private async Task<T> ParseBodyAsync<T>() where T : Resource
|
||||
@@ -145,6 +198,11 @@ public class FhirIngestController : ControllerBase
|
||||
return Parser.Parse<T>(json);
|
||||
}
|
||||
|
||||
private ContentResult Serialize(Resource resource) =>
|
||||
Content(Serializer.SerializeToString(resource), "application/fhir+json");
|
||||
}
|
||||
private ContentResult Serialize(Resource resource, int statusCode = StatusCodes.Status200OK) =>
|
||||
new()
|
||||
{
|
||||
Content = Serializer.SerializeToString(resource),
|
||||
ContentType = "application/fhir+json",
|
||||
StatusCode = statusCode
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user