fix security

This commit is contained in:
voltsrage
2026-06-21 19:53:19 +08:00
parent a37fad0e57
commit 7170b6efad
14 changed files with 422 additions and 27 deletions
@@ -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
};
}