fix security
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
using Hl7.Fhir.Model;
|
||||
using Hl7.Fhir.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
/// <summary>
|
||||
/// FHIR R4 read and search interactions for Patient and Encounter resources.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("fhir/R4")]
|
||||
[AuthorizePermission(ClinicalPermissions.FhirRead)]
|
||||
[ServiceFilter(typeof(FhirExceptionFilter))]
|
||||
public class FhirReadController : ControllerBase
|
||||
{
|
||||
private static readonly FhirJsonSerializer Serializer = new();
|
||||
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IExternalIdentifierService _identifiers;
|
||||
private readonly PatientFhirMapper _patientMapper;
|
||||
private readonly EncounterFhirMapper _encounterMapper;
|
||||
private readonly FhirOptions _options;
|
||||
private readonly ClinicalMetrics _metrics;
|
||||
|
||||
public FhirReadController(
|
||||
AppDbContext db,
|
||||
IExternalIdentifierService identifiers,
|
||||
PatientFhirMapper patientMapper,
|
||||
EncounterFhirMapper encounterMapper,
|
||||
IOptions<FhirOptions> options,
|
||||
ClinicalMetrics metrics)
|
||||
{
|
||||
_db = db;
|
||||
_identifiers = identifiers;
|
||||
_patientMapper = patientMapper;
|
||||
_encounterMapper = encounterMapper;
|
||||
_options = options.Value;
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a Patient resource by internal ID.
|
||||
/// </summary>
|
||||
[HttpGet("Patient/{id:guid}")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Hl7.Fhir.Model.Patient), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReadPatient(Guid id)
|
||||
{
|
||||
var patient = await _db.Patients.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (patient is null)
|
||||
throw new NotFoundException("Patient not found.", "FHIR_RESOURCE_NOT_FOUND");
|
||||
|
||||
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
||||
ExternalResourceType.Patient, patient.Id, _options.PatientIdentifierSystems);
|
||||
|
||||
var resource = _patientMapper.ToFhirResponse(patient, hospitalId);
|
||||
_metrics.FhirReadTotal.WithLabels("Patient", "read", "success").Inc();
|
||||
return Serialize(resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for Patient resources by identifier (system|value).
|
||||
/// </summary>
|
||||
[HttpGet("Patient")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Bundle), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> SearchPatient(
|
||||
[FromQuery] string? identifier,
|
||||
[FromQuery] int _count = 20)
|
||||
{
|
||||
_count = Math.Clamp(_count, 1, 100);
|
||||
|
||||
var bundle = new Bundle
|
||||
{
|
||||
Type = Bundle.BundleType.Searchset,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
if (identifier is not null)
|
||||
{
|
||||
var parts = identifier.Split('|', 2);
|
||||
if (parts.Length == 2 && !string.IsNullOrWhiteSpace(parts[0]) && !string.IsNullOrWhiteSpace(parts[1]))
|
||||
{
|
||||
var internalId = await _identifiers.ResolveInternalIdAsync(
|
||||
ExternalResourceType.Patient, parts[0], parts[1]);
|
||||
|
||||
if (internalId.HasValue)
|
||||
{
|
||||
var patient = await _db.Patients.AsNoTracking().FirstOrDefaultAsync(p => p.Id == internalId.Value);
|
||||
if (patient is not null)
|
||||
{
|
||||
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
||||
ExternalResourceType.Patient, patient.Id, _options.PatientIdentifierSystems);
|
||||
var resource = _patientMapper.ToFhirResponse(patient, hospitalId);
|
||||
bundle.Entry.Add(new Bundle.EntryComponent
|
||||
{
|
||||
FullUrl = $"{Request.Scheme}://{Request.Host}/fhir/R4/Patient/{patient.Id}",
|
||||
Resource = resource
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var patients = await _db.Patients
|
||||
.AsNoTracking()
|
||||
.OrderBy(p => p.LastName).ThenBy(p => p.FirstName)
|
||||
.Take(_count)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var patient in patients)
|
||||
{
|
||||
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
||||
ExternalResourceType.Patient, patient.Id, _options.PatientIdentifierSystems);
|
||||
var resource = _patientMapper.ToFhirResponse(patient, hospitalId);
|
||||
bundle.Entry.Add(new Bundle.EntryComponent
|
||||
{
|
||||
FullUrl = $"{Request.Scheme}://{Request.Host}/fhir/R4/Patient/{patient.Id}",
|
||||
Resource = resource
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bundle.Total = bundle.Entry.Count;
|
||||
_metrics.FhirReadTotal.WithLabels("Patient", "search", "success").Inc();
|
||||
return Serialize(bundle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an Encounter resource by internal ID.
|
||||
/// </summary>
|
||||
[HttpGet("Encounter/{id:guid}")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Hl7.Fhir.Model.Encounter), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReadEncounter(Guid id)
|
||||
{
|
||||
var encounter = await _db.Encounters
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Patient)
|
||||
.FirstOrDefaultAsync(e => e.Id == id);
|
||||
|
||||
if (encounter is null)
|
||||
throw new NotFoundException("Encounter not found.", "FHIR_RESOURCE_NOT_FOUND");
|
||||
|
||||
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
||||
ExternalResourceType.Encounter, encounter.Id, _options.EncounterIdentifierSystems);
|
||||
|
||||
var resource = _encounterMapper.ToFhirResponse(encounter, hospitalId);
|
||||
_metrics.FhirReadTotal.WithLabels("Encounter", "read", "success").Inc();
|
||||
return Serialize(resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for Encounter resources by patient reference.
|
||||
/// </summary>
|
||||
[HttpGet("Encounter")]
|
||||
[Produces("application/fhir+json")]
|
||||
[ProducesResponseType(typeof(Bundle), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> SearchEncounter(
|
||||
[FromQuery] string? patient,
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] int _count = 20)
|
||||
{
|
||||
_count = Math.Clamp(_count, 1, 100);
|
||||
|
||||
var query = _db.Encounters.AsNoTracking().Include(e => e.Patient).AsQueryable();
|
||||
|
||||
if (patient is not null && Guid.TryParse(patient, out var patientId))
|
||||
query = query.Where(e => e.PatientId == patientId);
|
||||
|
||||
if (status is not null)
|
||||
{
|
||||
var fhirStatus = status.ToLowerInvariant() switch
|
||||
{
|
||||
"in-progress" => EncounterStatus.Active,
|
||||
"finished" => EncounterStatus.Discharged,
|
||||
"cancelled" => EncounterStatus.Cancelled,
|
||||
_ => (EncounterStatus?)null
|
||||
};
|
||||
if (fhirStatus.HasValue)
|
||||
query = query.Where(e => e.Status == fhirStatus.Value);
|
||||
}
|
||||
|
||||
var encounters = await query
|
||||
.OrderByDescending(e => e.AdmittedAt)
|
||||
.Take(_count)
|
||||
.ToListAsync();
|
||||
|
||||
var bundle = new Bundle
|
||||
{
|
||||
Type = Bundle.BundleType.Searchset,
|
||||
Total = encounters.Count,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
foreach (var encounter in encounters)
|
||||
{
|
||||
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
||||
ExternalResourceType.Encounter, encounter.Id, _options.EncounterIdentifierSystems);
|
||||
var resource = _encounterMapper.ToFhirResponse(encounter, hospitalId);
|
||||
bundle.Entry.Add(new Bundle.EntryComponent
|
||||
{
|
||||
FullUrl = $"{Request.Scheme}://{Request.Host}/fhir/R4/Encounter/{encounter.Id}",
|
||||
Resource = resource
|
||||
});
|
||||
}
|
||||
|
||||
_metrics.FhirReadTotal.WithLabels("Encounter", "search", "success").Inc();
|
||||
return Serialize(bundle);
|
||||
}
|
||||
|
||||
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