From fea032ae47a145e8ba65902a2f29f0e4047208fa Mon Sep 17 00:00:00 2001 From: Christo du Toit Date: Thu, 26 Feb 2026 13:48:50 +0000 Subject: [PATCH 1/4] WIP --- .github/copilot-instructions.md | 219 ++ .github/workflows/build.yml | 150 + .github/workflows/prLinter.yml | 136 + LICENSE => LICENSE.txt | 2 +- ....Digital.ApiPlatform.Infrastructure.csproj | 15 + .../Program.cs | 23 + .../Services/ScriptGenerationService.cs | 201 ++ ...orm.Sdk.AspNetCore.Tests.Acceptance.csproj | 46 + .../appsettings.json | 2 + ...rm.Sdk.AspNetCore.Tests.Integration.csproj | 55 + .../appsettings.json | 2 + ...iPlatform.Sdk.AspNetCore.Tests.Unit.csproj | 37 + .../Storages/SessionApiPlatformStateBroker.cs | 67 + .../Storages/SessionApiPlatformStorageKeys.cs | 15 + .../Storages/SessionApiPlatformTokenBroker.cs | 128 + ....Digital.ApiPlatform.Sdk.AspNetCore.csproj | 66 + .../README.md | 173 + .../ServiceCollectionExtensions.cs | 35 + ...al.ApiPlatform.Sdk.Tests.Acceptance.csproj | 46 + .../appsettings.json | 2 + ...l.ApiPlatform.Sdk.Tests.Integration.csproj | 55 + .../appsettings.json | 2 + ....Digital.ApiPlatform.Sdk.Tests.Unit.csproj | 37 + .../Brokers/Cryptographies/CryptoBroker.cs | 23 + .../Brokers/Cryptographies/ICryptoBroker.cs | 11 + .../Brokers/DateTimes/DateTimeBroker.cs | 12 + .../Brokers/DateTimes/IDateTimeBroker.cs | 13 + .../Brokers/Https/HttpBroker.cs | 42 + .../Brokers/Https/IHttpBroker.cs | 24 + .../Brokers/Identifiers/IIdentifierBroker.cs | 12 + .../Brokers/Identifiers/IdentifierBroker.cs | 12 + .../Brokers/Serializations/IJsonBroker.cs | 12 + .../Brokers/Serializations/JsonBroker.cs | 19 + .../Storages/IApiPlatformStateBroker.cs | 15 + .../Storages/IApiPlatformTokenBroker.cs | 32 + .../Storages/MemoryApiPlatformStateBroker.cs | 42 + .../Storages/MemoryApiPlatformTokenBroker.cs | 88 + .../Clients/ApiPlatforms/ApiPlatformClient.cs | 93 + .../ApiPlatforms/ApiPlatformClientFacade.cs | 23 + .../ApiPlatforms/IApiPlatformClient.cs | 15 + .../CareIdentityServiceClient.cs | 182 + .../ICareIdentityServiceClient.cs | 49 + .../IPersonalDemographicsServiceClient.cs | 21 + .../PersonalDemographicsServiceClient.cs | 94 + ...dentityServiceClientDependencyException.cs | 16 + ...viceClientDependencyValidationException.cs | 16 + ...reIdentityServiceClientServiceException.cs | 16 + ...dentityServiceClientValidationException.cs | 16 + ...ailedCareIdentityServiceClientException.cs | 20 + ...sonalDemographicsServiceClientException.cs | 20 + ...raphicsServiceClientDependencyException.cs | 16 + ...viceClientDependencyValidationException.cs | 16 + ...mographicsServiceClientServiceException.cs | 16 + ...raphicsServiceClientValidationException.cs | 16 + .../ApiPlatformConfigurations.cs | 12 + .../CareIdentityConfigurations.cs | 20 + ...rsonalDemographicsServiceConfigurations.cs | 12 + .../CareIdentityServiceDependencyException.cs | 16 + ...ityServiceDependencyValidationException.cs | 16 + .../CareIdentityServiceServiceException.cs | 16 + .../CareIdentityServiceValidationException.cs | 16 + .../FailedCareIdentityServiceException.cs | 17 + ...lidArgumentCareIdentityServiceException.cs | 15 + ...nauthorisedCareIdentityServiceException.cs | 15 + .../CareIdentityServices/NhsUserInfo.cs | 47 + .../CareIdentityServices/TokenResult.cs | 29 + .../Exceptions/FailedPdsServiceException.cs | 17 + .../InvalidArgumentPdsServiceException.cs | 15 + .../PdsServiceDependencyException.cs | 16 + ...PdsServiceDependencyValidationException.cs | 16 + .../Pds/Exceptions/PdsServiceException.cs | 16 + .../PdsServiceValidationException.cs | 16 + .../FailedPdsOrchestrationException.cs | 17 + ...nvalidArgumentPdsOrchestrationException.cs | 15 + .../PdsOrchestrationDependencyException.cs | 16 + ...hestrationDependencyValidationException.cs | 16 + .../PdsOrchestrationServiceException.cs | 16 + .../PdsOrchestrationValidationException.cs | 16 + .../UnauthorizedPdsOrchestrationException.cs | 15 + ...ityServiceProcessingDependencyException.cs | 16 + ...ProcessingDependencyValidationException.cs | 16 + ...entityServiceProcessingServiceException.cs | 16 + ...ityServiceProcessingValidationException.cs | 16 + ...dCareIdentityServiceProcessingException.cs | 17 + ...tCareIdentityServiceProcessingException.cs | 15 + ...dCareIdentityServiceProcessingException.cs | 15 + .../NHS.Digital.ApiPlatform.Sdk.csproj | 65 + NHS.Digital.ApiPlatform.Sdk/README.md | 209 ++ .../ServiceCollectionExtensions.cs | 51 + .../CareIdentityService.Exceptions.cs | 89 + .../CareIdentityService.Validations.cs | 84 + .../CareIdentityService.cs | 240 ++ .../ICareIdentityService.cs | 18 + .../Services/Foundations/Pds/IPdsService.cs | 22 + .../Foundations/Pds/PdsService.Exceptions.cs | 58 + .../Foundations/Pds/PdsService.Validations.cs | 72 + .../Services/Foundations/Pds/PdsService.cs | 79 + .../Pds/IPdsOrchestrationService.cs | 20 + .../Pds/PdsOrchestrationService.Exceptions.cs | 115 + .../PdsOrchestrationService.Validations.cs | 70 + .../Pds/PdsOrchestrationService.cs | 48 + ...tityServiceProcessingService.Exceptions.cs | 85 + ...ityServiceProcessingService.Validations.cs | 58 + .../CareIdentityServiceProcessingService.cs | 56 + .../ICareIdentityServiceProcessingService.cs | 21 + NHS.Digital.ApiPlatform.slnx | 32 + README.md | 258 +- ReactApp1.Server/CHANGELOG.md | 8 + .../Controllers/AuthController.cs | 176 + .../Controllers/PatientController.cs | 46 + ReactApp1.Server/Data/ApplicationDbContext.cs | 29 + .../20260205171628_InitialCreate.Designer.cs | 69 + .../20260205171628_InitialCreate.cs | 46 + .../20260205171747_secondCreate.Designer.cs | 72 + .../Migrations/20260205171747_secondCreate.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 69 + ReactApp1.Server/Models/NhsUserInfo.cs | 42 + ReactApp1.Server/Models/User.cs | 13 + ReactApp1.Server/Program.cs | 100 + .../Properties/launchSettings.json | 26 + ReactApp1.Server/ReactApp1.Server.csproj | 38 + ReactApp1.Server/ReactApp1.Server.http | 6 + ReactApp1.Server/Services/ITokenService.cs | 6 + .../Services/SecureTokenStorage.cs | 213 ++ ReactApp1.Server/Services/TokenService.cs | 129 + ReactApp1.Server/TokenResult.cs | 22 + ReactApp1.Server/appsettings.json | 11 + Resources/Images/NhsDigitalBanner.png | Bin 0 -> 50568 bytes Resources/Images/NhsDigitalIcon.png | Bin 0 -> 57251 bytes reactapp1.client/.gitignore | 24 + reactapp1.client/CHANGELOG.md | 13 + reactapp1.client/README.md | 16 + reactapp1.client/eslint.config.js | 29 + reactapp1.client/index.html | 13 + reactapp1.client/package-lock.json | 2872 +++++++++++++++ reactapp1.client/package.json | 27 + reactapp1.client/public/vite.svg | 1 + reactapp1.client/reactapp1.client.esproj | 11 + reactapp1.client/src/App.css | 42 + reactapp1.client/src/App.jsx | 28 + reactapp1.client/src/PatientView.tsx | 10 + reactapp1.client/src/assets/react.svg | 1 + reactapp1.client/src/index.css | 68 + reactapp1.client/src/main.jsx | 9 + reactapp1.client/src/patient.jsx | 3102 +++++++++++++++++ reactapp1.client/src/types/fhir.tsx | 3 + reactapp1.client/src/viewer.jsx | 28 + reactapp1.client/vite.config.js | 87 + 148 files changed, 12346 insertions(+), 3 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/prLinter.yml rename LICENSE => LICENSE.txt (95%) create mode 100644 NHS.Digital.ApiPlatform.Infrastructure/NHS.Digital.ApiPlatform.Infrastructure.csproj create mode 100644 NHS.Digital.ApiPlatform.Infrastructure/Program.cs create mode 100644 NHS.Digital.ApiPlatform.Infrastructure/Services/ScriptGenerationService.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance.csproj create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance/appsettings.json create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration.csproj create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration/appsettings.json create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Unit/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Unit.csproj create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformStateBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformStorageKeys.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformTokenBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore/NHS.Digital.ApiPlatform.Sdk.AspNetCore.csproj create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore/README.md create mode 100644 NHS.Digital.ApiPlatform.Sdk.AspNetCore/ServiceCollectionExtensions.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance.csproj create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance/appsettings.json create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NHS.Digital.ApiPlatform.Sdk.Tests.Integration.csproj create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Unit/NHS.Digital.ApiPlatform.Sdk.Tests.Unit.csproj create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Cryptographies/CryptoBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Cryptographies/ICryptoBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/DateTimes/DateTimeBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/DateTimes/IDateTimeBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Https/HttpBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Https/IHttpBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Identifiers/IIdentifierBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Identifiers/IdentifierBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Serializations/IJsonBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Serializations/JsonBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/IApiPlatformStateBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/IApiPlatformTokenBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/MemoryApiPlatformStateBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/MemoryApiPlatformTokenBroker.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/ApiPlatformClient.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/ApiPlatformClientFacade.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/IApiPlatformClient.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Clients/CareIdentityServices/CareIdentityServiceClient.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Clients/CareIdentityServices/ICareIdentityServiceClient.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientDependencyException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientDependencyValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/FailedCareIdentityServiceClientException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/FailedPersonalDemographicsServiceClientException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientDependencyException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientDependencyValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Configurations/ApiPlatformConfigurations.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Configurations/CareIdentityConfigurations.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Configurations/PersonalDemographicsServiceConfigurations.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceDependencyException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceDependencyValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/FailedCareIdentityServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/InvalidArgumentCareIdentityServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/UnauthorisedCareIdentityServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/NhsUserInfo.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/TokenResult.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/FailedPdsServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/InvalidArgumentPdsServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceDependencyException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceDependencyValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/FailedPdsOrchestrationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/InvalidArgumentPdsOrchestrationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationDependencyException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationDependencyValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/UnauthorizedPdsOrchestrationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingDependencyException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingDependencyValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingServiceException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingValidationException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/FailedCareIdentityServiceProcessingException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/InvalidArgumentCareIdentityServiceProcessingException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/UnauthorisedCareIdentityServiceProcessingException.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj create mode 100644 NHS.Digital.ApiPlatform.Sdk/README.md create mode 100644 NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.Exceptions.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.Validations.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/ICareIdentityService.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.Exceptions.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.Validations.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.Exceptions.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.Validations.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.Exceptions.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.Validations.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/ICareIdentityServiceProcessingService.cs create mode 100644 NHS.Digital.ApiPlatform.slnx create mode 100644 ReactApp1.Server/CHANGELOG.md create mode 100644 ReactApp1.Server/Controllers/AuthController.cs create mode 100644 ReactApp1.Server/Controllers/PatientController.cs create mode 100644 ReactApp1.Server/Data/ApplicationDbContext.cs create mode 100644 ReactApp1.Server/Migrations/20260205171628_InitialCreate.Designer.cs create mode 100644 ReactApp1.Server/Migrations/20260205171628_InitialCreate.cs create mode 100644 ReactApp1.Server/Migrations/20260205171747_secondCreate.Designer.cs create mode 100644 ReactApp1.Server/Migrations/20260205171747_secondCreate.cs create mode 100644 ReactApp1.Server/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 ReactApp1.Server/Models/NhsUserInfo.cs create mode 100644 ReactApp1.Server/Models/User.cs create mode 100644 ReactApp1.Server/Program.cs create mode 100644 ReactApp1.Server/Properties/launchSettings.json create mode 100644 ReactApp1.Server/ReactApp1.Server.csproj create mode 100644 ReactApp1.Server/ReactApp1.Server.http create mode 100644 ReactApp1.Server/Services/ITokenService.cs create mode 100644 ReactApp1.Server/Services/SecureTokenStorage.cs create mode 100644 ReactApp1.Server/Services/TokenService.cs create mode 100644 ReactApp1.Server/TokenResult.cs create mode 100644 ReactApp1.Server/appsettings.json create mode 100644 Resources/Images/NhsDigitalBanner.png create mode 100644 Resources/Images/NhsDigitalIcon.png create mode 100644 reactapp1.client/.gitignore create mode 100644 reactapp1.client/CHANGELOG.md create mode 100644 reactapp1.client/README.md create mode 100644 reactapp1.client/eslint.config.js create mode 100644 reactapp1.client/index.html create mode 100644 reactapp1.client/package-lock.json create mode 100644 reactapp1.client/package.json create mode 100644 reactapp1.client/public/vite.svg create mode 100644 reactapp1.client/reactapp1.client.esproj create mode 100644 reactapp1.client/src/App.css create mode 100644 reactapp1.client/src/App.jsx create mode 100644 reactapp1.client/src/PatientView.tsx create mode 100644 reactapp1.client/src/assets/react.svg create mode 100644 reactapp1.client/src/index.css create mode 100644 reactapp1.client/src/main.jsx create mode 100644 reactapp1.client/src/patient.jsx create mode 100644 reactapp1.client/src/types/fhir.tsx create mode 100644 reactapp1.client/src/viewer.jsx create mode 100644 reactapp1.client/vite.config.js diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..dc0eab4 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,219 @@ + +# Copilot Instructions + +## Code Style Rules + +### Line Length + +- All `.cs` source files must adhere to the following rule: + - No line of code should exceed **120 characters** in length. + - This includes comments, string literals, and code. + - Exception: automatically generated files may be ignored if they cannot be reformatted safely. +- **How to measure (raw file characters, per-line only):** + - Count based on **raw file characters**, not editor rendering. + - **Tabs count as 4 characters** for measurement. + - Trailing whitespace must be removed. + - **Per-physical-line measurement ONLY.** The unit of measurement is a **single newline-delimited line**. + - **Never** add or aggregate the lengths of multiple lines. + - A wrapped invocation is compliant if **each** physical line is ≤ 120 characters. + - Ignore soft wrapping (on-screen wrapping that doesn't insert a newline). + +### Code Formatting + +- Single-line instructions must follow each other with **no blank lines** in between. +- **New rule (clarified):** A multi-line instruction must be preceded by **exactly one blank line _only when it begins a new statement_**. + Do **not** require a blank line before a multi-line **continuation** of an existing statement. +- If a multi-line instruction is followed by further instructions, it must also be followed by **exactly one blank line**. +- **Exception (block first statement):** If a statement is the **first statement inside a block**—i.e., directly after `{`—**no preceding blank line** is required. +- Any C# `return` statement must be preceded by **exactly one blank line** unless it is the first statement in a block. +- If a constructor/method name would push a line past 120 characters, move `new`, the call, or the arguments to the next line. +- Always format so that **no single physical line exceeds 120 characters**, even when calls span multiple lines. +- **Definition of a blank line (updated):** + A blank line is any physical line that contains **no visible characters**. After trimming whitespace, the line must be empty. + Lines containing only spaces or tabs **are valid blank lines**. +- **Method separation:** Method declarations must be preceded by **exactly one blank line** after the closing brace of the previous member. +- **Argument indentation:** + - For multi-line method or constructor calls, the first line ends before the first argument. + - Each wrapped argument line must be indented **one additional indentation level** (usually 4 spaces). + - Do **not** use extra indentation levels. + - The closing `)` must align with the start of the call. +- **Continuation clarification (applies across all checks):** + - A line is considered a **continuation of the same statement** and must **not** be flagged for a missing blank line when **both** are true: + 1) The previous non-empty trimmed line **does not** end with `;` or `}`, **and** + 2) The current line, after trimming leading whitespace, **starts with a continuation indicator**, such as: + `.`, `??`, `?`, `:`, `+`, `-`, `*`, `/`, `%`, `&&`, `||`, `=>`, `,`, `)`, `]`, + or any identifier/keyword when the previous line ends with an incomplete construct (e.g., open `(`, interpolated start `$"`, method/constructor call, LINQ chain). + - Only when a **new statement** begins (i.e., the previous trimmed line **ends** with `;` or `}`) and the **next statement is multi-line** should an **exactly one** blank line be required before it. + +### Enforcement + +- Copilot should **not generate code** that exceeds the 120-character line limit. +- When writing new C# code, Copilot should: + - Break up long method/constructor calls across multiple lines. + - Use string interpolation or verbatim strings with proper line breaks where needed. + - Format long LINQ queries across multiple lines. + - Wrap parameters and arguments for readability. + - Insert a blank line before any `return` following other statements. + - Prefer moving `new` or the method invocation to the next line when appropriate. + +### Review Guidelines (strict) + +- Copilot must: + - Evaluate **each physical line independently**. + - Flag a violation only when a **single physical line** exceeds 120 characters. + - When flagging, include line number and measured character count. + - Suggest multiline formatting only when the offending line exceeds 120. + - **Not** flag whitespace-only lines; they are valid blank lines. + - **Continuation Detection (unambiguous):** Do **not** require a blank line before a line that is a continuation of the same statement. + Treat a line as a continuation when **both** of the following hold: + 1) The previous non-empty trimmed line **does not** end with `;` or `}`, **and** + 2) The current line (after trimming leading whitespace) **begins with** a continuation indicator: + `.`, `??`, `?`, `:`, `+`, `-`, `*`, `/`, `%`, `&&`, `||`, `=>`, `,`, `)`, `]`, + or any identifier/keyword when the previous line ends with an incomplete construct (e.g., open `(`, start of `$"`, method/constructor call, LINQ chain). + - Flag missing blank lines before `return` **only** when `return` is the first token on the line **and** the previous non-empty trimmed line ended with `;` or `}`. +- **Operator lines:** + - Measure compliance per physical line. + - Do not combine operator lines with continuations. + - Operator-at-end style is preferred. +- **Block-first statement exemption:** + - Do not require a preceding blank line if the previous meaningful line ends with `{`. + +### Examples + +#### ✅ Correct (first statement inside a block; no blank line required) + +```csharp +public void Foo() +{ + DoSomething( + x, + y); +} +``` + +#### ❌ Incorrect (blank line required between two statements) + +```csharp +DoSomething(); +DoSomethingElse( + x, + y); +``` + +#### ✅ Correct (wrapped invocation; each line ≤ 120) + +```csharp +Validate( + createException: () => new InvalidDecisionPollException( + message: "Invalid decisionPoll. Please correct the errors and try again."), + (Rule: IsInvalid(decisionPoll.Id), Parameter: nameof(DecisionPoll.Id))); +``` + +#### ❌ Incorrect (single line > 120) + +```csharp +Validate(createException: () => new InvalidDecisionPollException(message: "Invalid decisionPoll. Please correct the errors and try again.")); +``` + +--- + +### Code Formatting Rule Examples + +#### ✅ Correct (return with blank line) + +```csharp +var user = users.FirstOrDefault(u => u.Id == id); + +return user; +``` + +#### ❌ Incorrect + +```csharp +var user = users.FirstOrDefault(u => u.Id == id); +return user; +``` + +--- + +### Argument Indentation Examples + +#### ✅ Correct + +```csharp +DoSomething( + firstArgument: "value1", + secondArgument: "value2", + thirdArgument: "value3"); +``` + +#### ❌ Incorrect (extra indentation) + +```csharp +DoSomething( + firstArgument: "value1", + secondArgument: "value2", + thirdArgument: "value3"); +``` + +#### ❌ Incorrect (misaligned closing parenthesis) + +```csharp +DoSomething( + firstArgument: "value1", + secondArgument: "value2", + thirdArgument: "value3" + ); +``` + +--- + +### More Formatting Examples + +#### ✅ Correct + +```csharp +var filteredUsers = users + .Where(u => u.IsActive && u.LastLoginDate >= DateTime.UtcNow.AddDays(-30)) + .OrderByDescending(u => u.LastLoginDate) + .Select(u => new + { + u.Id, + u.Name, + u.Email, + LastSeen = u.LastLoginDate.ToString("yyyy-MM-dd HH:mm:ss") + }) + .ToList(); +``` + +#### ❌ Incorrect + +```csharp +var filteredUsers = users.Where(u => u.IsActive && u.LastLoginDate >= DateTime.UtcNow.AddDays(-30)).OrderByDescending(u => u.LastLoginDate).Select(u => new { u.Id, u.Name, u.Email, LastSeen = u.LastLoginDate.ToString("yyyy-MM-dd HH:mm:ss") }).ToList(); +``` + +--- + +### Rationale + +- **Per-line measurement** prevents false positives in wrapped calls. +- **Tabs count as 4 characters** ensures consistent line-length calculation. +- **Whitespace-only blank lines count as blank** and match VS behaviour. +- **Return visibility** improves readability. +- **Argument indentation** improves consistency. +- **Block-first patterns** avoid unnecessary whitespace noise. + +--- + +## Supporting .editorconfig Settings + +```ini +[*.{cs,vb,ts,tsx}] +guidelines = 120 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +end_of_line = crlf + +dotnet_sort_system_directives_first = true +``` diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e305b38 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,150 @@ +name: Build +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + - reopened + - closed + branches: + - main +jobs: + build: + name: Build + runs-on: windows-latest + steps: + - name: Enable long paths for Git + run: git config --system core.longpaths true + - name: Check out + uses: actions/checkout@v3 + - name: Setup .Net + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 10.0.100 + - name: Restore + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Run Unit Tests + run: >- + $projects = Get-ChildItem -Path . -Filter "*Tests.Unit*.csproj" -Recurse + + foreach ($project in $projects) { + Write-Host "Running tests for: $($project.FullName)" + dotnet test $project.FullName --no-build --verbosity normal + } + shell: pwsh + - name: Run Acceptance Tests + run: >- + $projects = Get-ChildItem -Path . -Filter "*Tests.Acceptance*.csproj" -Recurse + + foreach ($project in $projects) { + Write-Host "Running tests for: $($project.FullName)" + dotnet test $project.FullName --no-build --verbosity normal + } + add_tag: + name: Tag and Release + runs-on: ubuntu-latest + needs: + - build + if: >- + needs.build.result == 'success' && + + github.event.pull_request.merged && + + github.event.pull_request.base.ref == 'main' && + + startsWith(github.event.pull_request.title, 'RELEASES:') && + + contains(github.event.pull_request.labels.*.name, 'RELEASES') + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT_FOR_TAGGING }} + - name: Configure Git + run: >- + git config user.name "GitHub Action" + + git config user.email "action@github.com" + - name: Extract Version + id: extract_version + run: > + # Running on Linux/Unix + + sudo apt-get install xmlstarlet + + version_number=$(xmlstarlet sel -t -v "//Version" -n NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj) + + echo "$version_number" + + echo "version_number<> $GITHUB_OUTPUT + + echo "$version_number" >> $GITHUB_OUTPUT + + echo "EOF" >> $GITHUB_OUTPUT + shell: bash + - name: Display Version + run: 'echo "Version number: ${{ steps.extract_version.outputs.version_number }}"' + - name: Extract Package Release Notes + id: extract_package_release_notes + run: > + # Running on Linux/Unix + + sudo apt-get install xmlstarlet + + package_release_notes=$(xmlstarlet sel -t -v "//PackageReleaseNotes" -n NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj) + + echo "$package_release_notes" + + echo "package_release_notes<> $GITHUB_OUTPUT + + echo "$package_release_notes" >> $GITHUB_OUTPUT + + echo "EOF" >> $GITHUB_OUTPUT + shell: bash + - name: Display Package Release Notes + run: 'echo "Package Release Notes: ${{ steps.extract_package_release_notes.outputs.package_release_notes }}"' + - name: Create GitHub Tag + run: >- + git tag -a "v${{ steps.extract_version.outputs.version_number }}" -m "Release - v${{ steps.extract_version.outputs.version_number }}" + + git push origin --tags + - name: Create GitHub Release + uses: actions/create-release@v1 + with: + tag_name: v${{ steps.extract_version.outputs.version_number }} + release_name: Release - v${{ steps.extract_version.outputs.version_number }} + body: >- + ## Release - v${{ steps.extract_version.outputs.version_number }} + + + ### Release Notes + + ${{ steps.extract_package_release_notes.outputs.package_release_notes }} + env: + GITHUB_TOKEN: ${{ secrets.PAT_FOR_TAGGING }} + publish: + name: Publish to NuGet + runs-on: ubuntu-latest + needs: + - add_tag + if: needs.add_tag.result == 'success' + steps: + - name: Check out + uses: actions/checkout@v3 + - name: Setup .Net + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 10.0.100 + - name: Restore + run: dotnet restore + - name: Build + run: dotnet build --no-restore --configuration Release + - name: Pack NuGet Package + run: dotnet pack --configuration Release --include-symbols + - name: Push NuGet Package + run: dotnet nuget push **/bin/Release/**/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_ACCESS }} --skip-duplicate diff --git a/.github/workflows/prLinter.yml b/.github/workflows/prLinter.yml new file mode 100644 index 0000000..ec62d16 --- /dev/null +++ b/.github/workflows/prLinter.yml @@ -0,0 +1,136 @@ +name: PR Linter +on: + pull_request: + types: + - opened + - edited + - synchronize + - reopened + - closed + branches: + - main +jobs: + label: + name: Label + runs-on: ubuntu-latest + steps: + - name: Apply Label + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: >- + const prefixes = [ + 'INFRA:', + 'PROVISIONS:', + 'RELEASES:', + 'DATA:', + 'BROKERS:', + 'FOUNDATIONS:', + 'PROCESSINGS:', + 'ORCHESTRATIONS:', + 'COORDINATIONS:', + 'MANAGEMENTS:', + 'AGGREGATIONS:', + 'CONTROLLERS:', + 'CLIENTS:', + 'EXPOSERS:', + 'PROVIDERS:', + 'BASE:', + 'COMPONENTS:', + 'VIEWS:', + 'PAGES:', + 'ACCEPTANCE:', + 'INTEGRATIONS:', + 'CODE RUB:', + 'MINOR FIX:', + 'MEDIUM FIX:', + 'MAJOR FIX:', + 'DOCUMENTATION:', + 'CONFIG:', + 'STANDARD:', + 'DESIGN:', + 'BUSINESS:' + ]; + + + const pullRequest = context.payload.pull_request; + + + if (!pullRequest) { + console.log('No pull request context available.'); + return; + } + + + const title = context.payload.pull_request.title; + + const existingLabels = context.payload.pull_request.labels.map(label => label.name); + + + for (const prefix of prefixes) { + if (title.startsWith(prefix)) { + const label = prefix.slice(0, -1); + if (!existingLabels.includes(label)) { + console.log(`Applying label: ${label}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: [label] + }); + } + break; + } + } + permissions: + contents: read + pull-requests: write + issues: write + requireIssueOrTask: + name: Require Issue Or Task Association + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v3 + - name: Get PR Information + id: get_pr_info + uses: actions/github-script@v6 + with: + script: >2- + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number + }); + + const prOwner = pr.data.user.login || ""; + const prBody = pr.data.body || ""; + core.setOutput("prOwner", prOwner); + core.setOutput("description", prBody); + console.log(`PR Owner: ${prOwner}`); + console.log(`PR Body: ${prBody}`); + - name: Check For Associated Issues Or Tasks + id: check_for_issues_or_tasks + if: ${{ steps.get_pr_info.outputs.prOwner != 'dependabot[bot]' }} + run: >2- + PR_BODY="${{ steps.get_pr_info.outputs.description }}" + echo "::notice::Raw PR Body: $PR_BODY" + + if [[ -z "$PR_BODY" ]]; then + echo "Error: PR description does not contain any links to issue(s)/task(s) (e.g., 'closes #123' / 'closes AB#123' / 'fixes #123' / 'fixes AB#123')." + exit 1 + fi + + PR_BODY=$(echo "$PR_BODY" | tr -s '\r\n' ' ' | tr '\n' ' ' | xargs) + echo "::notice::Normalized PR Body: $PR_BODY" + + if echo "$PR_BODY" | grep -Piq "((close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*(\[#\d+\]|\#\d+)|(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*(\[AB#\d+\]|AB#\d+))"; then + echo "Valid PR description." + else + echo "Error: PR description does not contain any links to issue(s)/task(s) (e.g., 'closes #123' / 'closes AB#123' / 'fixes #123' / 'fixes AB#123')." + exit 1 + fi + shell: bash + permissions: + contents: read + pull-requests: read diff --git a/LICENSE b/LICENSE.txt similarity index 95% rename from LICENSE rename to LICENSE.txt index 18b5282..091681e 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Intelligence Solutions for London +Copyright (c) 2024 Intelligence Solutions for London Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NHS.Digital.ApiPlatform.Infrastructure/NHS.Digital.ApiPlatform.Infrastructure.csproj b/NHS.Digital.ApiPlatform.Infrastructure/NHS.Digital.ApiPlatform.Infrastructure.csproj new file mode 100644 index 0000000..36a9403 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Infrastructure/NHS.Digital.ApiPlatform.Infrastructure.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + disable + disable + + + + + + + + diff --git a/NHS.Digital.ApiPlatform.Infrastructure/Program.cs b/NHS.Digital.ApiPlatform.Infrastructure/Program.cs new file mode 100644 index 0000000..dc55f81 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Infrastructure/Program.cs @@ -0,0 +1,23 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using NHS.Digital.ApiPlatform.Infrastructure.Services; + +namespace NHS.Digital.ApiPlatform.Infrastructure +{ + internal class Program + { + static void Main(string[] args) + { + var scriptGenerationService = new ScriptGenerationService(); + + scriptGenerationService.GenerateBuildScript( + branchName: "main", + projectName: "NHS.Digital.ApiPlatform.Sdk", + dotNetVersion: "10.0.100"); + + scriptGenerationService.GeneratePrLintScript(branchName: "main"); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Infrastructure/Services/ScriptGenerationService.cs b/NHS.Digital.ApiPlatform.Infrastructure/Services/ScriptGenerationService.cs new file mode 100644 index 0000000..f4b0c08 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Infrastructure/Services/ScriptGenerationService.cs @@ -0,0 +1,201 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System.Collections.Generic; +using System.IO; +using ADotNet.Clients; +using ADotNet.Models.Pipelines.GithubPipelines.DotNets; +using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks; +using ADotNet.Models.Pipelines.GithubPipelines.DotNets.Tasks.SetupDotNetTaskV3s; + +namespace NHS.Digital.ApiPlatform.Infrastructure.Services +{ + internal class ScriptGenerationService + { + private readonly ADotNetClient adotNetClient; + + public ScriptGenerationService() => + adotNetClient = new ADotNetClient(); + + public void GenerateBuildScript(string branchName, string projectName, string dotNetVersion) + { + var githubPipeline = new GithubPipeline + { + Name = "Build", + + OnEvents = new Events + { + Push = new PushEvent { Branches = [branchName] }, + + PullRequest = new PullRequestEvent + { + Types = ["opened", "synchronize", "reopened", "closed"], + Branches = [branchName] + } + }, + + Jobs = new Dictionary + { + { + "build", + new Job + { + Name = "Build", + RunsOn = BuildMachines.WindowsLatest, + + Steps = new List + { + new GithubTask + { + Name = "Enable long paths for Git", + Run = "git config --system core.longpaths true" + }, + + new CheckoutTaskV3 + { + Name = "Check out" + }, + + new SetupDotNetTaskV3 + { + Name = "Setup .Net", + + With = new TargetDotNetVersionV3 + { + DotNetVersion = dotNetVersion + } + }, + + new RestoreTask + { + Name = "Restore" + }, + + new DotNetBuildTask + { + Name = "Build" + }, + + new TestTask + { + Name = "Run Unit Tests", + Shell = "pwsh", + Run = + """ + $projects = Get-ChildItem -Path . -Filter "*Tests.Unit*.csproj" -Recurse + foreach ($project in $projects) { + Write-Host "Running tests for: $($project.FullName)" + dotnet test $project.FullName --no-build --verbosity normal + } + """ + }, + + new TestTask + { + Name = "Run Acceptance Tests", + Run = + """ + $projects = Get-ChildItem -Path . -Filter "*Tests.Acceptance*.csproj" -Recurse + foreach ($project in $projects) { + Write-Host "Running tests for: $($project.FullName)" + dotnet test $project.FullName --no-build --verbosity normal + } + """ + } + } + } + }, + { + "add_tag", + new TagJob( + runsOn: BuildMachines.UbuntuLatest, + dependsOn: "build", + projectRelativePath: $"{projectName}/{projectName}.csproj", + githubToken: "${{ secrets.PAT_FOR_TAGGING }}", + branchName: branchName) + { + Name = "Tag and Release" + } + }, + { + "publish", + new PublishJobV2( + runsOn: BuildMachines.UbuntuLatest, + dependsOn: "add_tag", + dotNetVersion: dotNetVersion, + nugetApiKey: "${{ secrets.NUGET_ACCESS }}") + { + Name = "Publish to NuGet" + } + } + } + }; + + string buildScriptPath = "../../../../.github/workflows/build.yml"; + string directoryPath = Path.GetDirectoryName(buildScriptPath); + + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + adotNetClient.SerializeAndWriteToFile( + adoPipeline: githubPipeline, + path: buildScriptPath); + } + + public void GeneratePrLintScript(string branchName) + { + var githubPipeline = new GithubPipeline + { + Name = "PR Linter", + + OnEvents = new Events + { + PullRequest = new PullRequestEvent + { + Types = ["opened", "edited", "synchronize", "reopened", "closed"], + Branches = [branchName] + } + }, + + Jobs = new Dictionary + { + { + "label", + new LabelJobV2(runsOn: BuildMachines.UbuntuLatest) + { + Name = "Label", + Permissions = new Dictionary + { + { "contents", "read" }, + { "pull-requests", "write" }, + { "issues", "write" } + } + } + }, + { + "requireIssueOrTask", + new RequireIssueOrTaskJob() + { + Name = "Require Issue Or Task Association", + } + }, + } + }; + + string buildScriptPath = "../../../../.github/workflows/prLinter.yml"; + string directoryPath = Path.GetDirectoryName(buildScriptPath); + + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + adotNetClient.SerializeAndWriteToFile( + adoPipeline: githubPipeline, + path: buildScriptPath); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance.csproj b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance.csproj new file mode 100644 index 0000000..c530045 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance.csproj @@ -0,0 +1,46 @@ + + + + net10.0 + disable + disable + false + true + CS1998 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + Always + + + Always + + + + \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance/appsettings.json b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance/appsettings.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Acceptance/appsettings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration.csproj b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration.csproj new file mode 100644 index 0000000..40f9c9c --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration.csproj @@ -0,0 +1,55 @@ + + + + net10.0 + disable + disable + false + true + CS1998 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + Always + + + Always + + + Always + + + + diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration/appsettings.json b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration/appsettings.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Integration/appsettings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Unit/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Unit.csproj b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Unit/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Unit.csproj new file mode 100644 index 0000000..41ae512 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Unit/NHS.Digital.ApiPlatform.Sdk.AspNetCore.Tests.Unit.csproj @@ -0,0 +1,37 @@ + + + + net10.0 + disable + disable + false + true + CS1998 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformStateBroker.cs b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformStateBroker.cs new file mode 100644 index 0000000..5f49dd5 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformStateBroker.cs @@ -0,0 +1,67 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; + +namespace NHS.Digital.ApiPlatform.Sdk.AspNetCore.Brokers.Storages +{ + internal sealed class SessionApiPlatformStateBroker : IApiPlatformStateBroker + { + private readonly IHttpContextAccessor httpContextAccessor; + + public SessionApiPlatformStateBroker(IHttpContextAccessor httpContextAccessor) => + this.httpContextAccessor = httpContextAccessor; + + public ValueTask StoreCsrfStateAsync(string state, CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + session.SetString(SessionApiPlatformStorageKeys.CsrfState, state); + + return ValueTask.CompletedTask; + } + + public ValueTask GetCsrfStateAsync(CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + string? state = session.GetString(SessionApiPlatformStorageKeys.CsrfState); + + return ValueTask.FromResult(state); + } + + public ValueTask ClearCsrfStateAsync(CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + session.Remove(SessionApiPlatformStorageKeys.CsrfState); + + return ValueTask.CompletedTask; + } + + private ISession GetSessionOrThrow() + { + HttpContext? httpContext = this.httpContextAccessor.HttpContext; + + if (httpContext is null) + { + throw new InvalidOperationException( + "No active HttpContext. Ensure this code runs within an ASP.NET Core request pipeline."); + } + + // Accessing Session will throw if session middleware is not configured. + try + { + return httpContext.Session; + } + catch (InvalidOperationException exception) + { + throw new InvalidOperationException( + "Session is not available. Ensure you have configured session services (services.AddSession) and middleware (app.UseSession).", + exception); + } + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformStorageKeys.cs b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformStorageKeys.cs new file mode 100644 index 0000000..30d9bd6 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformStorageKeys.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +namespace NHS.Digital.ApiPlatform.Sdk.AspNetCore.Brokers.Storages +{ + internal static class SessionApiPlatformStorageKeys + { + internal const string CsrfState = "Nhs.ApiPlatform.CsrfState"; + internal const string AccessToken = "Nhs.ApiPlatform.AccessToken"; + internal const string AccessTokenExpiresAtUtc = "Nhs.ApiPlatform.AccessToken.ExpiresAtUtc"; + internal const string RefreshToken = "Nhs.ApiPlatform.RefreshToken"; + internal const string RefreshTokenExpiresAtUtc = "Nhs.ApiPlatform.RefreshToken.ExpiresAtUtc"; + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformTokenBroker.cs b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformTokenBroker.cs new file mode 100644 index 0000000..3a7c925 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/Brokers/Storages/SessionApiPlatformTokenBroker.cs @@ -0,0 +1,128 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; + +namespace NHS.Digital.ApiPlatform.Sdk.AspNetCore.Brokers.Storages +{ + internal sealed class SessionApiPlatformTokenBroker : IApiPlatformTokenBroker + { + private readonly IHttpContextAccessor httpContextAccessor; + + public SessionApiPlatformTokenBroker(IHttpContextAccessor httpContextAccessor) => + this.httpContextAccessor = httpContextAccessor; + + public ValueTask StoreAccessTokenAsync( + string accessToken, + DateTimeOffset expiresAtUtc, + CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + + session.SetString(SessionApiPlatformStorageKeys.AccessToken, accessToken); + session.SetString( + SessionApiPlatformStorageKeys.AccessTokenExpiresAtUtc, + expiresAtUtc.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)); + + return ValueTask.CompletedTask; + } + + public ValueTask<(string? Token, DateTimeOffset? ExpiresAtUtc)> GetAccessTokenAsync( + CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + + string? token = session.GetString(SessionApiPlatformStorageKeys.AccessToken); + DateTimeOffset? expiresAtUtc = ReadExpiresAtUtc(session, SessionApiPlatformStorageKeys.AccessTokenExpiresAtUtc); + + return ValueTask.FromResult((token, expiresAtUtc)); + } + + public ValueTask ClearAccessTokenAsync(CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + + session.Remove(SessionApiPlatformStorageKeys.AccessToken); + session.Remove(SessionApiPlatformStorageKeys.AccessTokenExpiresAtUtc); + + return ValueTask.CompletedTask; + } + + public ValueTask StoreRefreshTokenAsync( + string refreshToken, + DateTimeOffset expiresAtUtc, + CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + + session.SetString(SessionApiPlatformStorageKeys.RefreshToken, refreshToken); + session.SetString( + SessionApiPlatformStorageKeys.RefreshTokenExpiresAtUtc, + expiresAtUtc.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)); + + return ValueTask.CompletedTask; + } + + public ValueTask<(string? Token, DateTimeOffset? ExpiresAtUtc)> GetRefreshTokenAsync( + CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + + string? token = session.GetString(SessionApiPlatformStorageKeys.RefreshToken); + DateTimeOffset? expiresAtUtc = ReadExpiresAtUtc(session, SessionApiPlatformStorageKeys.RefreshTokenExpiresAtUtc); + + return ValueTask.FromResult((token, expiresAtUtc)); + } + + public ValueTask ClearRefreshTokenAsync(CancellationToken cancellationToken = default) + { + ISession session = GetSessionOrThrow(); + + session.Remove(SessionApiPlatformStorageKeys.RefreshToken); + session.Remove(SessionApiPlatformStorageKeys.RefreshTokenExpiresAtUtc); + + return ValueTask.CompletedTask; + } + + private static DateTimeOffset? ReadExpiresAtUtc(ISession session, string expiresKey) + { + string? expiresAt = session.GetString(expiresKey); + + if (string.IsNullOrWhiteSpace(expiresAt)) + { + return null; + } + + bool parsed = long.TryParse(expiresAt, NumberStyles.Integer, CultureInfo.InvariantCulture, out long seconds); + return parsed ? DateTimeOffset.FromUnixTimeSeconds(seconds) : null; + } + + private ISession GetSessionOrThrow() + { + HttpContext? httpContext = this.httpContextAccessor.HttpContext; + + if (httpContext is null) + { + throw new InvalidOperationException( + "No active HttpContext. Ensure this code runs within an ASP.NET Core request pipeline."); + } + + try + { + return httpContext.Session; + } + catch (InvalidOperationException exception) + { + throw new InvalidOperationException( + "Session is not available. Ensure you have configured session services (services.AddSession) and middleware (app.UseSession).", + exception); + } + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore/NHS.Digital.ApiPlatform.Sdk.AspNetCore.csproj b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/NHS.Digital.ApiPlatform.Sdk.AspNetCore.csproj new file mode 100644 index 0000000..012eff6 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/NHS.Digital.ApiPlatform.Sdk.AspNetCore.csproj @@ -0,0 +1,66 @@ + + + + net10.0 + disable + disable + + NHS.Digital.ApiPlatform.Sdk.AspNetCore + NHS.Digital.ApiPlatform.Sdk.AspNetCore + NHS.Digital.ApiPlatform.Sdk.AspNetCore + North East London ICB + North East London ICB + + NHS Digital API Platform Client. + + North East London ICB - 2026 (c) + NhsDigitalIcon.png + https://github.com/NHSISL/NHS.Digital.ApiPlatform + https://github.com/NHSISL/NHS.Digital.ApiPlatform + git + NHSISL; NHS Digital; API; Platform; Client; .NET; The Standard; + + Initial release of the NHS Digital API Platform Client. + + True + 0.1.0.0 + 0.1.0.0 + 0.1.0.0 + README.md + LICENSE.txt + true + True + CS1998 + + + + + true + + Always + + + True + + + + True + + + + + + + + + + + + + + + + + + + diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore/README.md b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/README.md new file mode 100644 index 0000000..d5ac847 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/README.md @@ -0,0 +1,173 @@ +# NHS.Digital.ApiPlatform.Sdk.AspNetCore + +## Overview + +`NHS.Digital.ApiPlatform.Sdk.AspNetCore` is the ASP.NET Core adapter +for: + +`NHS.Digital.ApiPlatform.Sdk` + +It provides: + +- Session-based token/state storage +- Cookie-based token/state storage (BFF-style) +- DI registration helpers +- Seamless integration into ASP.NET Core applications + +------------------------------------------------------------------------ + +## Installation + +``` bash +dotnet add package NHS.Digital.ApiPlatform.Sdk +dotnet add package NHS.Digital.ApiPlatform.Sdk.AspNetCore +``` + +------------------------------------------------------------------------ + +## Configuration (appsettings.json) + +``` json +{ + "ApiPlatform": { + "CareIdentity": { + "ClientId": "...", + "ClientSecret": "...", + "RedirectUri": "...", + "AuthEndpoint": "...", + "TokenEndpoint": "...", + "UserInfoEndpoint": "...", + "AcrValues": "aal3" + }, + "PersonalDemographicsService": { + "BaseUrl": "..." + } + } +} +``` + +------------------------------------------------------------------------ + +## Registration + +### Session Mode (Recommended to Start) + +``` csharp +builder.Services.AddDistributedMemoryCache(); +builder.Services.AddSession(); + +builder.Services.AddApiPlatformSdkAspNetCore( + builder.Configuration.GetSection("ApiPlatform") + .Get(), + storageMode: ApiPlatformAspNetStorageMode.Session); + +app.UseSession(); +``` + +### Cookie Mode (BFF-Style) + +``` csharp +builder.Services.AddApiPlatformSdkAspNetCore( + builder.Configuration.GetSection("ApiPlatform") + .Get(), + storageMode: ApiPlatformAspNetStorageMode.Cookies); +``` + +Cookies are: - HttpOnly - Secure (when HTTPS) - SameSite=Lax + +------------------------------------------------------------------------ + +## Example Auth Controller + +``` csharp +[ApiController] +[Route("auth")] +public sealed class AuthController : ControllerBase +{ + private readonly IApiPlatformClient api; + + public AuthController(IApiPlatformClient api) => this.api = api; + + [HttpGet("login")] + public IActionResult Login() + { + string url = this.api.CareIdentityServices.Login(); + return Redirect(url); + } + + [HttpGet("callback")] + public IActionResult Callback(string code, string state) + { + this.api.CareIdentityServices.Callback(code, state); + return Redirect("/"); + } + + [HttpPost("logout")] + public IActionResult Logout() + { + this.api.CareIdentityServices.Logout(); + return Redirect("/"); + } +} +``` + +------------------------------------------------------------------------ + +## Example PDS Controller + +``` csharp +[ApiController] +[Route("pds")] +public sealed class PdsController : ControllerBase +{ + private readonly IApiPlatformClient api; + + public PdsController(IApiPlatformClient api) => this.api = api; + + [HttpGet("patients")] + public async Task Search(string family) + { + string result = await this.api + .PersonalDemographicsServices + .SearchPatientsAsync(family); + + return Content(result, "application/fhir+json"); + } +} +``` + +------------------------------------------------------------------------ + +## Refresh Token Renewal + +All calls automatically use: + +``` csharp +GetAccessToken() +``` + +If expired, the SDK: + +1. Uses refresh token +2. Calls token endpoint +3. Stores new tokens +4. Continues execution + +No extra developer code required. + +------------------------------------------------------------------------ + +## Requirements + +### Session Mode + +- `AddSession()` +- `UseSession()` + +### Cookie Mode + +- HTTPS recommended for production + +------------------------------------------------------------------------ + +© North East London ICB diff --git a/NHS.Digital.ApiPlatform.Sdk.AspNetCore/ServiceCollectionExtensions.cs b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..2d9ae1e --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.AspNetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; +using NHS.Digital.ApiPlatform.Sdk.AspNetCore.Brokers.Storages; + +namespace NHS.Digital.ApiPlatform.Sdk.AspNetCore +{ + public static class ServiceCollectionExtensions + { + /// + /// Registers ASP.NET Core specific storage brokers for the NHS Digital API Platform SDK. + /// + /// This wiring enables per-user storage via . + /// The host application must also configure session middleware: + /// - services.AddDistributedMemoryCache() (or your distributed cache) + /// - services.AddSession(...) + /// - app.UseSession() + /// + public static IServiceCollection AddApiPlatformSdkAspNetCore(this IServiceCollection services) + { + services.TryAddSingleton(); + + // Override the SDK's in-memory defaults with session-backed implementations. + services.AddScoped(); + services.AddScoped(); + + return services; + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance.csproj b/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance.csproj new file mode 100644 index 0000000..28ea862 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance.csproj @@ -0,0 +1,46 @@ + + + + net10.0 + disable + disable + false + true + CS1998 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + Always + + + Always + + + + \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance/appsettings.json b/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance/appsettings.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Acceptance/appsettings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NHS.Digital.ApiPlatform.Sdk.Tests.Integration.csproj b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NHS.Digital.ApiPlatform.Sdk.Tests.Integration.csproj new file mode 100644 index 0000000..3897a1a --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NHS.Digital.ApiPlatform.Sdk.Tests.Integration.csproj @@ -0,0 +1,55 @@ + + + + net10.0 + disable + disable + false + true + CS1998 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + Always + + + Always + + + Always + + + + diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Unit/NHS.Digital.ApiPlatform.Sdk.Tests.Unit.csproj b/NHS.Digital.ApiPlatform.Sdk.Tests.Unit/NHS.Digital.ApiPlatform.Sdk.Tests.Unit.csproj new file mode 100644 index 0000000..b14dcb8 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Unit/NHS.Digital.ApiPlatform.Sdk.Tests.Unit.csproj @@ -0,0 +1,37 @@ + + + + net10.0 + disable + disable + false + true + CS1998 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Cryptographies/CryptoBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Cryptographies/CryptoBroker.cs new file mode 100644 index 0000000..660e63d --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Cryptographies/CryptoBroker.cs @@ -0,0 +1,23 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Security.Cryptography; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Cryptographies +{ + internal sealed class CryptoBroker : ICryptoBroker + { + public string CreateUrlSafeState(int bytes = 32) + { + byte[] stateBytes = new byte[bytes]; + RandomNumberGenerator.Fill(stateBytes); + + return Convert.ToBase64String(stateBytes) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Cryptographies/ICryptoBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Cryptographies/ICryptoBroker.cs new file mode 100644 index 0000000..c888f34 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Cryptographies/ICryptoBroker.cs @@ -0,0 +1,11 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Cryptographies +{ + public interface ICryptoBroker + { + string CreateUrlSafeState(int bytes = 32); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/DateTimes/DateTimeBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/DateTimes/DateTimeBroker.cs new file mode 100644 index 0000000..8760fab --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/DateTimes/DateTimeBroker.cs @@ -0,0 +1,12 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.DateTimes +{ + internal class DateTimeBroker : IDateTimeBroker + { + public DateTimeOffset GetCurrentDateTimeOffset() => DateTimeOffset.UtcNow; + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/DateTimes/IDateTimeBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/DateTimes/IDateTimeBroker.cs new file mode 100644 index 0000000..20899e3 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/DateTimes/IDateTimeBroker.cs @@ -0,0 +1,13 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.DateTimes +{ + internal interface IDateTimeBroker + { + DateTimeOffset GetCurrentDateTimeOffset(); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Https/HttpBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Https/HttpBroker.cs new file mode 100644 index 0000000..8a68706 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Https/HttpBroker.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Https +{ + internal sealed class HttpBroker : IHttpBroker + { + private readonly IHttpClientFactory httpClientFactory; + + public HttpBroker(IHttpClientFactory httpClientFactory) => + this.httpClientFactory = httpClientFactory; + + public async ValueTask PostFormAsync( + string url, + IEnumerable> formValues, + CancellationToken cancellationToken) + { + HttpClient client = this.httpClientFactory.CreateClient("NhsApiPlatform"); + var content = new FormUrlEncodedContent(formValues); + + return await client.PostAsync(url, content, cancellationToken); + } + + public async ValueTask GetAsync( + string url, + Action? configureRequest, + CancellationToken cancellationToken) + { + HttpClient client = this.httpClientFactory.CreateClient("NhsApiPlatform"); + var request = new HttpRequestMessage(HttpMethod.Get, url); + configureRequest?.Invoke(request); + + return await client.SendAsync(request, cancellationToken); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Https/IHttpBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Https/IHttpBroker.cs new file mode 100644 index 0000000..fff8af4 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Https/IHttpBroker.cs @@ -0,0 +1,24 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Https +{ + public interface IHttpBroker + { + ValueTask PostFormAsync( + string url, + IEnumerable> formValues, + CancellationToken cancellationToken); + + ValueTask GetAsync( + string url, + Action? configureRequest, + CancellationToken cancellationToken); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Identifiers/IIdentifierBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Identifiers/IIdentifierBroker.cs new file mode 100644 index 0000000..0b162da --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Identifiers/IIdentifierBroker.cs @@ -0,0 +1,12 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Identifiers +{ + public interface IIdentifierBroker + { + Guid GetNewGuid(); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Identifiers/IdentifierBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Identifiers/IdentifierBroker.cs new file mode 100644 index 0000000..d286560 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Identifiers/IdentifierBroker.cs @@ -0,0 +1,12 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Identifiers +{ + internal sealed class IdentifierBroker : IIdentifierBroker + { + public Guid GetNewGuid() => Guid.NewGuid(); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Serializations/IJsonBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Serializations/IJsonBroker.cs new file mode 100644 index 0000000..daf1260 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Serializations/IJsonBroker.cs @@ -0,0 +1,12 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Serializations +{ + public interface IJsonBroker + { + T? Deserialize(string json); + string Serialize(object value); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Serializations/JsonBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Serializations/JsonBroker.cs new file mode 100644 index 0000000..066a102 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Serializations/JsonBroker.cs @@ -0,0 +1,19 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System.Text.Json; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Serializations +{ + internal sealed class JsonBroker : IJsonBroker + { + private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web); + + public T? Deserialize(string json) => + JsonSerializer.Deserialize(json, Options); + + public string Serialize(object value) => + JsonSerializer.Serialize(value, Options); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/IApiPlatformStateBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/IApiPlatformStateBroker.cs new file mode 100644 index 0000000..20ccb56 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/IApiPlatformStateBroker.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Storages +{ + public interface IApiPlatformStateBroker + { + ValueTask StoreCsrfStateAsync(string state, CancellationToken cancellationToken = default); + ValueTask GetCsrfStateAsync(CancellationToken cancellationToken = default); + ValueTask ClearCsrfStateAsync(CancellationToken cancellationToken = default); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/IApiPlatformTokenBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/IApiPlatformTokenBroker.cs new file mode 100644 index 0000000..7d5350d --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/IApiPlatformTokenBroker.cs @@ -0,0 +1,32 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Storages +{ + public interface IApiPlatformTokenBroker + { + ValueTask StoreAccessTokenAsync( + string accessToken, + DateTimeOffset expiresAtUtc, + CancellationToken cancellationToken = default); + + ValueTask<(string? Token, DateTimeOffset? ExpiresAtUtc)> GetAccessTokenAsync( + CancellationToken cancellationToken = default); + + ValueTask ClearAccessTokenAsync(CancellationToken cancellationToken = default); + + ValueTask StoreRefreshTokenAsync( + string refreshToken, + DateTimeOffset expiresAtUtc, + CancellationToken cancellationToken = default); + + ValueTask<(string? Token, DateTimeOffset? ExpiresAtUtc)> GetRefreshTokenAsync( + CancellationToken cancellationToken = default); + + ValueTask ClearRefreshTokenAsync(CancellationToken cancellationToken = default); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/MemoryApiPlatformStateBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/MemoryApiPlatformStateBroker.cs new file mode 100644 index 0000000..98766a5 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/MemoryApiPlatformStateBroker.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Storages +{ + internal sealed class MemoryApiPlatformStateBroker : IApiPlatformStateBroker + { + private readonly object locker = new(); + private string? csrfState; + + public ValueTask StoreCsrfStateAsync(string state, CancellationToken cancellationToken = default) + { + lock (this.locker) + { + this.csrfState = state; + } + + return ValueTask.CompletedTask; + } + + public ValueTask GetCsrfStateAsync(CancellationToken cancellationToken = default) + { + lock (this.locker) + { + return ValueTask.FromResult(this.csrfState); + } + } + + public ValueTask ClearCsrfStateAsync(CancellationToken cancellationToken = default) + { + lock (this.locker) + { + this.csrfState = null; + } + + return ValueTask.CompletedTask; + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/MemoryApiPlatformTokenBroker.cs b/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/MemoryApiPlatformTokenBroker.cs new file mode 100644 index 0000000..107f85e --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Brokers/Storages/MemoryApiPlatformTokenBroker.cs @@ -0,0 +1,88 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Brokers.Storages +{ + internal sealed class MemoryApiPlatformTokenBroker : IApiPlatformTokenBroker + { + private readonly object locker = new(); + + private string? accessToken; + private DateTimeOffset? accessExpiresAtUtc; + + private string? refreshToken; + private DateTimeOffset? refreshExpiresAtUtc; + + public ValueTask StoreAccessTokenAsync( + string accessToken, + DateTimeOffset expiresAtUtc, + CancellationToken cancellationToken = default) + { + lock (this.locker) + { + this.accessToken = accessToken; + this.accessExpiresAtUtc = expiresAtUtc; + } + + return ValueTask.CompletedTask; + } + + public ValueTask<(string? Token, DateTimeOffset? ExpiresAtUtc)> GetAccessTokenAsync( + CancellationToken cancellationToken = default) + { + lock (this.locker) + { + return ValueTask.FromResult((this.accessToken, this.accessExpiresAtUtc)); + } + } + + public ValueTask ClearAccessTokenAsync(CancellationToken cancellationToken = default) + { + lock (this.locker) + { + this.accessToken = null; + this.accessExpiresAtUtc = null; + } + + return ValueTask.CompletedTask; + } + + public ValueTask StoreRefreshTokenAsync( + string refreshToken, + DateTimeOffset expiresAtUtc, + CancellationToken cancellationToken = default) + { + lock (this.locker) + { + this.refreshToken = refreshToken; + this.refreshExpiresAtUtc = expiresAtUtc; + } + + return ValueTask.CompletedTask; + } + + public ValueTask<(string? Token, DateTimeOffset? ExpiresAtUtc)> GetRefreshTokenAsync( + CancellationToken cancellationToken = default) + { + lock (this.locker) + { + return ValueTask.FromResult((this.refreshToken, this.refreshExpiresAtUtc)); + } + } + + public ValueTask ClearRefreshTokenAsync(CancellationToken cancellationToken = default) + { + lock (this.locker) + { + this.refreshToken = null; + this.refreshExpiresAtUtc = null; + } + + return ValueTask.CompletedTask; + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/ApiPlatformClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/ApiPlatformClient.cs new file mode 100644 index 0000000..cfd9d35 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/ApiPlatformClient.cs @@ -0,0 +1,93 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Microsoft.Extensions.DependencyInjection; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; +using NHS.Digital.ApiPlatform.Sdk.Clients.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices; +using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; + +namespace NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms +{ + public sealed class ApiPlatformClient : IApiPlatformClient + { + // Standalone/quick-start constructor (no DI knowledge required) + // Uses in-memory storage defaults. + public ApiPlatformClient(ApiPlatformConfigurations apiPlatformConfigurations) + { + IServiceProvider serviceProvider = + BuildStandaloneServiceProvider( + apiPlatformConfigurations, + apiPlatformStateBroker: null, + apiPlatformTokenBroker: null); + + InitializeClients(serviceProvider); + } + + // Standalone factory for non-DI hosts that want custom storage brokers. + // Falls back to in-memory brokers if none are supplied. + public static IApiPlatformClient Create( + ApiPlatformConfigurations apiPlatformConfigurations, + IApiPlatformStateBroker apiPlatformStateBroker = null, + IApiPlatformTokenBroker apiPlatformTokenBroker = null) + { + IServiceProvider serviceProvider = + BuildStandaloneServiceProvider( + apiPlatformConfigurations, + apiPlatformStateBroker, + apiPlatformTokenBroker); + + return serviceProvider.GetRequiredService(); + } + + // DI constructor (ASP.NET Core will use this) + public ApiPlatformClient( + ICareIdentityServiceClient careIdentityServiceClient, + IPersonalDemographicsServiceClient personalDemographicsServiceClient) + { + CareIdentityServiceClient = careIdentityServiceClient; + PersonalDemographicsServiceClient = personalDemographicsServiceClient; + } + + public ICareIdentityServiceClient CareIdentityServiceClient { get; private set; } + public IPersonalDemographicsServiceClient PersonalDemographicsServiceClient { get; private set; } + + private void InitializeClients(IServiceProvider serviceProvider) + { + CareIdentityServiceClient = + serviceProvider.GetRequiredService(); + + PersonalDemographicsServiceClient = + serviceProvider.GetRequiredService(); + } + + private static IServiceProvider BuildStandaloneServiceProvider( + ApiPlatformConfigurations apiPlatformConfigurations, + IApiPlatformStateBroker apiPlatformStateBroker, + IApiPlatformTokenBroker apiPlatformTokenBroker) + { + IServiceCollection services = new ServiceCollection(); + + // Shared core registrations: + services.AddApiPlatformSdkCore(apiPlatformConfigurations); + + // Optional custom brokers (non-DI hosts): + if (apiPlatformStateBroker is not null) + { + services.AddSingleton(_ => apiPlatformStateBroker); + } + + if (apiPlatformTokenBroker is not null) + { + services.AddSingleton(_ => apiPlatformTokenBroker); + } + + // Standalone defaults only (applies if custom brokers were not provided): + services.AddApiPlatformSdkInMemoryStorage(); + + return services.BuildServiceProvider(); + } + } +} \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/ApiPlatformClientFacade.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/ApiPlatformClientFacade.cs new file mode 100644 index 0000000..6d2d7b7 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/ApiPlatformClientFacade.cs @@ -0,0 +1,23 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using NHS.Digital.ApiPlatform.Sdk.Clients.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices; + +namespace NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms +{ + internal sealed class ApiPlatformClientFacade : IApiPlatformClient + { + public ApiPlatformClientFacade( + ICareIdentityServiceClient careIdentityServiceClient, + IPersonalDemographicsServiceClient personalDemographicsServiceClient) + { + CareIdentityServiceClient = careIdentityServiceClient; + PersonalDemographicsServiceClient = personalDemographicsServiceClient; + } + + public ICareIdentityServiceClient CareIdentityServiceClient { get; } + public IPersonalDemographicsServiceClient PersonalDemographicsServiceClient { get; } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/IApiPlatformClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/IApiPlatformClient.cs new file mode 100644 index 0000000..4f711d6 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/ApiPlatforms/IApiPlatformClient.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using NHS.Digital.ApiPlatform.Sdk.Clients.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices; + +namespace NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms +{ + public interface IApiPlatformClient + { + ICareIdentityServiceClient CareIdentityServiceClient { get; } + IPersonalDemographicsServiceClient PersonalDemographicsServiceClient { get; } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/CareIdentityServices/CareIdentityServiceClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/CareIdentityServices/CareIdentityServiceClient.cs new file mode 100644 index 0000000..202f43a --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/CareIdentityServices/CareIdentityServiceClient.cs @@ -0,0 +1,182 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Clients.CareIdentityService.Exceptions; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions; +using NHS.Digital.ApiPlatform.Sdk.Services.Processings.CareIdentityServices; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Clients.CareIdentityServices +{ + internal sealed class CareIdentityServiceClient : ICareIdentityServiceClient + { + private readonly ICareIdentityServiceProcessingService careIdentityServiceProcessingService; + + public CareIdentityServiceClient(ICareIdentityServiceProcessingService careIdentityServiceProcessingService) => + this.careIdentityServiceProcessingService = careIdentityServiceProcessingService; + + public async ValueTask BuildLoginUrlAsync(CancellationToken cancellationToken = default) + { + try + { + return await this.careIdentityServiceProcessingService.BuildLoginUrlAsync(cancellationToken); + } + catch (CareIdentityServiceProcessingValidationException careIdentityServiceProcessingValidationException) + { + throw CreateCareIdentityServiceClientValidationException( + careIdentityServiceProcessingValidationException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingDependencyValidationException + careIdentityServiceProcessingDependencyValidationException) + { + throw CreateCareIdentityServiceClientValidationException( + careIdentityServiceProcessingDependencyValidationException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingDependencyException careIdentityServiceProcessingDependencyException) + { + throw CreateCareIdentityServiceClientDependencyException( + careIdentityServiceProcessingDependencyException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingServiceException careIdentityServiceProcessingServiceException) + { + throw CreateCareIdentityServiceClientServiceException( + careIdentityServiceProcessingServiceException.InnerException as Xeption); + } + catch (Exception exception) + { + throw CreateCareIdentityServiceClientServiceException( + new FailedCareIdentityServiceClientException( + message: "Unexpected error occurred, contact support.", + innerException: exception, + data: exception.Data)); + } + } + + public async ValueTask LogoutAsync(CancellationToken cancellationToken = default) + { + try + { + await this.careIdentityServiceProcessingService.LogoutAsync(cancellationToken); + } + catch (CareIdentityServiceProcessingValidationException careIdentityServiceProcessingValidationException) + { + throw CreateCareIdentityServiceClientValidationException( + careIdentityServiceProcessingValidationException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingDependencyValidationException + careIdentityServiceProcessingDependencyValidationException) + { + throw CreateCareIdentityServiceClientValidationException( + careIdentityServiceProcessingDependencyValidationException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingDependencyException careIdentityServiceProcessingDependencyException) + { + throw CreateCareIdentityServiceClientDependencyException( + careIdentityServiceProcessingDependencyException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingServiceException careIdentityServiceProcessingServiceException) + { + throw CreateCareIdentityServiceClientServiceException( + careIdentityServiceProcessingServiceException.InnerException as Xeption); + } + catch (Exception exception) + { + throw CreateCareIdentityServiceClientServiceException( + new FailedCareIdentityServiceClientException( + message: "Unexpected error occurred, contact support.", + innerException: exception, + data: exception.Data)); + } + } + + public async ValueTask GetAccessTokenAsync(CancellationToken cancellationToken = default) + { + try + { + return await this.careIdentityServiceProcessingService.GetAccessTokenAsync(cancellationToken); + } + catch (CareIdentityServiceProcessingValidationException careIdentityServiceProcessingValidationException) + { + throw CreateCareIdentityServiceClientValidationException( + careIdentityServiceProcessingValidationException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingDependencyValidationException + careIdentityServiceProcessingDependencyValidationException) + { + throw CreateCareIdentityServiceClientValidationException( + careIdentityServiceProcessingDependencyValidationException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingDependencyException careIdentityServiceProcessingDependencyException) + { + throw CreateCareIdentityServiceClientDependencyException( + careIdentityServiceProcessingDependencyException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingServiceException careIdentityServiceProcessingServiceException) + { + throw CreateCareIdentityServiceClientServiceException( + careIdentityServiceProcessingServiceException.InnerException as Xeption); + } + } + + public async ValueTask GetUserInfoAsync( + string code, + string state, + CancellationToken cancellationToken = default) + { + try + { + return await this.careIdentityServiceProcessingService.GetUserInfoAsync(code, state, cancellationToken); + } + catch (CareIdentityServiceProcessingValidationException careIdentityServiceProcessingValidationException) + { + throw CreateCareIdentityServiceClientValidationException( + careIdentityServiceProcessingValidationException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingDependencyValidationException + careIdentityServiceProcessingDependencyValidationException) + { + throw CreateCareIdentityServiceClientValidationException( + careIdentityServiceProcessingDependencyValidationException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingDependencyException careIdentityServiceProcessingDependencyException) + { + throw CreateCareIdentityServiceClientDependencyException( + careIdentityServiceProcessingDependencyException.InnerException as Xeption); + } + catch (CareIdentityServiceProcessingServiceException careIdentityServiceProcessingServiceException) + { + throw CreateCareIdentityServiceClientServiceException( + careIdentityServiceProcessingServiceException.InnerException as Xeption); + } + } + + private static CareIdentityServiceClientValidationException + CreateCareIdentityServiceClientValidationException(Xeption innerException) + { + return new CareIdentityServiceClientValidationException( + message: "Care identity service client validation error occurred, fix errors and try again.", + innerException); + } + + private static CareIdentityServiceClientDependencyException + CreateCareIdentityServiceClientDependencyException(Xeption innerException) + { + return new CareIdentityServiceClientDependencyException( + message: "Care identity service client dependency error occurred, contact support.", + innerException); + } + + private static CareIdentityServiceClientServiceException + CreateCareIdentityServiceClientServiceException(Xeption innerException) + { + return new CareIdentityServiceClientServiceException( + message: "Care identity service client service error occurred, contact support.", + innerException); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/CareIdentityServices/ICareIdentityServiceClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/CareIdentityServices/ICareIdentityServiceClient.cs new file mode 100644 index 0000000..098b101 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/CareIdentityServices/ICareIdentityServiceClient.cs @@ -0,0 +1,49 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System.Threading; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices; + +namespace NHS.Digital.ApiPlatform.Sdk.Clients.CareIdentityServices +{ + public interface ICareIdentityServiceClient + { + /// + /// Logs in to the Care Identity Service. + /// + /// Returns a redirection URL. + ValueTask BuildLoginUrlAsync(CancellationToken cancellationToken = default); + + /// + /// Logs out of the Care Identity Service. + /// + /// Returns a redirection URL. + ValueTask LogoutAsync(CancellationToken cancellationToken = default); + + /// + /// Retrieves the access token used to authenticate API requests. + /// + /// A string containing the access token required for authorized API calls. + ValueTask GetAccessTokenAsync(CancellationToken cancellationToken = default); + + /// + /// Retrieves the current CIS2 user information for the provided access token. + /// + /// + /// The code to be processed by the callback. + /// Typically represents an authorization or verification code + /// received from an external source. + /// + /// + /// The state information associated with the callback. + /// Used to maintain context or verify the integrity of the operation. + /// + /// The user information associated with the access token. + ValueTask GetUserInfoAsync( + string code, + string state, + CancellationToken cancellationToken = default); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs new file mode 100644 index 0000000..6bb95df --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs @@ -0,0 +1,21 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices +{ + public interface IPersonalDemographicsServiceClient + { + ValueTask SearchPatientsAsync( + string family, + IEnumerable? given = null, + string? gender = null, + DateOnly? birthdate = null, + CancellationToken cancellationToken = default); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs new file mode 100644 index 0000000..4eaab79 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs @@ -0,0 +1,94 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Clients.Pds.Exceptions; +using NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions; +using NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices +{ + internal class PersonalDemographicsServiceClient : IPersonalDemographicsServiceClient + { + private readonly IPdsOrchestrationService pdsOrchestrationService; + + public PersonalDemographicsServiceClient(IPdsOrchestrationService pdsOrchestrationService) => + this.pdsOrchestrationService = pdsOrchestrationService; + + public async ValueTask SearchPatientsAsync( + string family, + IEnumerable given = null, + string gender = null, + DateOnly? birthdate = null, + CancellationToken cancellationToken = default) + { + try + { + return await this.pdsOrchestrationService.SearchPatientsAsync( + family, + given, + gender, + birthdate, + cancellationToken); + } + catch (PdsOrchestrationValidationException pdsOrchestrationValidationException) + { + throw CreatePersonalDemographicsServiceClientValidationException( + pdsOrchestrationValidationException.InnerException as Xeption); + } + catch (PdsOrchestrationDependencyValidationException + pdsOrchestrationDependencyValidationException) + { + throw CreatePersonalDemographicsServiceClientValidationException( + pdsOrchestrationDependencyValidationException.InnerException as Xeption); + } + catch (PdsOrchestrationDependencyException pdsOrchestrationDependencyException) + { + throw CreatePersonalDemographicsServiceClientDependencyException( + pdsOrchestrationDependencyException.InnerException as Xeption); + } + catch (PdsOrchestrationServiceException pdsOrchestrationServiceException) + { + throw CreatePersonalDemographicsServiceClientServiceException( + pdsOrchestrationServiceException.InnerException as Xeption); + } + catch (Exception exception) + { + throw CreatePersonalDemographicsServiceClientServiceException( + new FailedPersonalDemographicsServiceClientException( + message: "Unexpected error occurred, contact support.", + innerException: exception, + data: exception.Data)); + } + } + + private static PersonalDemographicsServiceClientValidationException + CreatePersonalDemographicsServiceClientValidationException(Xeption innerException) + { + return new PersonalDemographicsServiceClientValidationException( + message: "Personal demographics service client validation error occurred, fix errors and try again.", + innerException); + } + + private static PersonalDemographicsServiceClientDependencyException + CreatePersonalDemographicsServiceClientDependencyException(Xeption innerException) + { + return new PersonalDemographicsServiceClientDependencyException( + message: "Personal demographics service client dependency error occurred, contact support.", + innerException); + } + + private static PersonalDemographicsServiceClientServiceException + CreatePersonalDemographicsServiceClientServiceException(Xeption innerException) + { + return new PersonalDemographicsServiceClientServiceException( + message: "Personal demographics service client service error occurred, contact support.", + innerException); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientDependencyException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientDependencyException.cs new file mode 100644 index 0000000..985cad6 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientDependencyException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.CareIdentityService.Exceptions +{ + public class CareIdentityServiceClientDependencyException : Xeption + { + public CareIdentityServiceClientDependencyException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientDependencyValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientDependencyValidationException.cs new file mode 100644 index 0000000..5d13958 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientDependencyValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.CareIdentityService.Exceptions +{ + public class CareIdentityServiceClientDependencyValidationException : Xeption + { + public CareIdentityServiceClientDependencyValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientServiceException.cs new file mode 100644 index 0000000..9a3cd59 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientServiceException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.CareIdentityService.Exceptions +{ + public class CareIdentityServiceClientServiceException : Xeption + { + public CareIdentityServiceClientServiceException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientValidationException.cs new file mode 100644 index 0000000..754da45 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/CareIdentityServiceClientValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.CareIdentityService.Exceptions +{ + public class CareIdentityServiceClientValidationException : Xeption + { + public CareIdentityServiceClientValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/FailedCareIdentityServiceClientException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/FailedCareIdentityServiceClientException.cs new file mode 100644 index 0000000..9ea976e --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/CareIdentityService/Exceptions/FailedCareIdentityServiceClientException.cs @@ -0,0 +1,20 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.CareIdentityService.Exceptions +{ + public class FailedCareIdentityServiceClientException : Xeption + { + public FailedCareIdentityServiceClientException( + string message, + Exception innerException, + IDictionary data) + : base(message, innerException, data) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/FailedPersonalDemographicsServiceClientException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/FailedPersonalDemographicsServiceClientException.cs new file mode 100644 index 0000000..1fb6452 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/FailedPersonalDemographicsServiceClientException.cs @@ -0,0 +1,20 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.Pds.Exceptions +{ + public class FailedPersonalDemographicsServiceClientException : Xeption + { + public FailedPersonalDemographicsServiceClientException( + string message, + Exception innerException, + IDictionary data) + : base(message, innerException, data) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientDependencyException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientDependencyException.cs new file mode 100644 index 0000000..600edc7 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientDependencyException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.Pds.Exceptions +{ + public class PersonalDemographicsServiceClientDependencyException : Xeption + { + public PersonalDemographicsServiceClientDependencyException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientDependencyValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientDependencyValidationException.cs new file mode 100644 index 0000000..410de8a --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientDependencyValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.Pds.Exceptions +{ + public class PersonalDemographicsServiceClientDependencyValidationException : Xeption + { + public PersonalDemographicsServiceClientDependencyValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientServiceException.cs new file mode 100644 index 0000000..9d5eb06 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientServiceException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.Pds.Exceptions +{ + public class PersonalDemographicsServiceClientServiceException : Xeption + { + public PersonalDemographicsServiceClientServiceException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientValidationException.cs new file mode 100644 index 0000000..c6b241e --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Clients/Pds/Exceptions/PersonalDemographicsServiceClientValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Clients.Pds.Exceptions +{ + public class PersonalDemographicsServiceClientValidationException : Xeption + { + public PersonalDemographicsServiceClientValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/ApiPlatformConfigurations.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/ApiPlatformConfigurations.cs new file mode 100644 index 0000000..74da1f1 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/ApiPlatformConfigurations.cs @@ -0,0 +1,12 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Configurations +{ + public class ApiPlatformConfigurations + { + public CareIdentityConfigurations CareIdentity { get; set; } = new(); + public PersonalDemographicsServiceConfigurations PersonalDemographicsService { get; set; } = new(); + } +} \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/CareIdentityConfigurations.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/CareIdentityConfigurations.cs new file mode 100644 index 0000000..edf76c2 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/CareIdentityConfigurations.cs @@ -0,0 +1,20 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Configurations +{ + public class CareIdentityConfigurations + { + public string ClientId { get; set; } = string.Empty; + public string ClientSecret { get; set; } = string.Empty; + public string RedirectUri { get; set; } = string.Empty; + + public string AuthEndpoint { get; set; } = string.Empty; + public string TokenEndpoint { get; set; } = string.Empty; + public string UserInfoEndpoint { get; set; } = string.Empty; + + // Optional - e.g. "aal3" + public string? AcrValues { get; set; } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/PersonalDemographicsServiceConfigurations.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/PersonalDemographicsServiceConfigurations.cs new file mode 100644 index 0000000..7340926 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Configurations/PersonalDemographicsServiceConfigurations.cs @@ -0,0 +1,12 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Configurations +{ + public class PersonalDemographicsServiceConfigurations + { + // e.g. "https://int.api.service.nhs.uk/personal-demographics/FHIR/R4" + public string BaseUrl { get; set; } = string.Empty; + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceDependencyException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceDependencyException.cs new file mode 100644 index 0000000..6ebb377 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceDependencyException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions +{ + public class CareIdentityServiceDependencyException : Xeption + { + public CareIdentityServiceDependencyException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceDependencyValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceDependencyValidationException.cs new file mode 100644 index 0000000..0977248 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceDependencyValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions +{ + public class CareIdentityServiceDependencyValidationException : Xeption + { + public CareIdentityServiceDependencyValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceServiceException.cs new file mode 100644 index 0000000..2d39ebd --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceServiceException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions +{ + public class CareIdentityServiceServiceException : Xeption + { + public CareIdentityServiceServiceException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceValidationException.cs new file mode 100644 index 0000000..59c13ba --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/CareIdentityServiceValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions +{ + public class CareIdentityServiceValidationException : Xeption + { + public CareIdentityServiceValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/FailedCareIdentityServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/FailedCareIdentityServiceException.cs new file mode 100644 index 0000000..9145de0 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/FailedCareIdentityServiceException.cs @@ -0,0 +1,17 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions +{ + public class FailedCareIdentityServiceException : Xeption + { + public FailedCareIdentityServiceException(string message, Exception innerException, IDictionary data) + : base(message, innerException, data) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/InvalidArgumentCareIdentityServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/InvalidArgumentCareIdentityServiceException.cs new file mode 100644 index 0000000..8c2d6ff --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/InvalidArgumentCareIdentityServiceException.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions +{ + public class InvalidArgumentCareIdentityServiceException : Xeption + { + public InvalidArgumentCareIdentityServiceException(string message) + : base(message) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/UnauthorisedCareIdentityServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/UnauthorisedCareIdentityServiceException.cs new file mode 100644 index 0000000..ea8e3d2 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/Exceptions/UnauthorisedCareIdentityServiceException.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions +{ + public class UnauthorisedCareIdentityServiceException : Xeption + { + public UnauthorisedCareIdentityServiceException(string message) + : base(message) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/NhsUserInfo.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/NhsUserInfo.cs new file mode 100644 index 0000000..88fd448 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/NhsUserInfo.cs @@ -0,0 +1,47 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices +{ + public sealed class NhsUserInfo + { + [JsonPropertyName("nhsid_useruid")] + public string NhsIdUserUid { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("nhsid_nrbac_roles")] + public List NhsIdNrbacRoles { get; set; } = new(); + + [JsonPropertyName("sub")] + public string Sub { get; set; } = string.Empty; + } + + public sealed class NhsNrbacRole + { + [JsonPropertyName("person_orgid")] + public string PersonOrgId { get; set; } = string.Empty; + + [JsonPropertyName("person_roleid")] + public string PersonRoleId { get; set; } = string.Empty; + + [JsonPropertyName("org_code")] + public string OrgCode { get; set; } = string.Empty; + + [JsonPropertyName("role_name")] + public string RoleName { get; set; } = string.Empty; + + [JsonPropertyName("role_code")] + public string RoleCode { get; set; } = string.Empty; + + [JsonPropertyName("activities")] + public List Activities { get; set; } = new(); + + [JsonPropertyName("activity_codes")] + public List ActivityCodes { get; set; } = new(); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/TokenResult.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/TokenResult.cs new file mode 100644 index 0000000..ed1bfe3 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/CareIdentityServices/TokenResult.cs @@ -0,0 +1,29 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System.Text.Json.Serialization; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices +{ + public sealed class TokenResult + { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = string.Empty; + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } = string.Empty; + + [JsonPropertyName("expires_in")] + public string ExpiresIn { get; set; } = "0"; + + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } = string.Empty; + + [JsonPropertyName("refresh_token_expires_in")] + public string RefreshTokenExpiresIn { get; set; } = "0"; + + [JsonPropertyName("id_token")] + public string? IdToken { get; set; } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/FailedPdsServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/FailedPdsServiceException.cs new file mode 100644 index 0000000..940e6f8 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/FailedPdsServiceException.cs @@ -0,0 +1,17 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions +{ + public class FailedPdsServiceException : Xeption + { + public FailedPdsServiceException(string message, Exception innerException, IDictionary data) + : base(message, innerException, data) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/InvalidArgumentPdsServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/InvalidArgumentPdsServiceException.cs new file mode 100644 index 0000000..c8edba5 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/InvalidArgumentPdsServiceException.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions +{ + public class InvalidArgumentPdsServiceException : Xeption + { + public InvalidArgumentPdsServiceException(string message) + : base(message) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceDependencyException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceDependencyException.cs new file mode 100644 index 0000000..c5c9f41 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceDependencyException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions +{ + public class PdsServiceDependencyException : Xeption + { + public PdsServiceDependencyException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceDependencyValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceDependencyValidationException.cs new file mode 100644 index 0000000..1b0f966 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceDependencyValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions +{ + public class PdsServiceDependencyValidationException : Xeption + { + public PdsServiceDependencyValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceException.cs new file mode 100644 index 0000000..fa3a169 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions +{ + public class PdsServiceException : Xeption + { + public PdsServiceException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceValidationException.cs new file mode 100644 index 0000000..8981111 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/Exceptions/PdsServiceValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions +{ + public class PdsServiceValidationException : Xeption + { + public PdsServiceValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/FailedPdsOrchestrationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/FailedPdsOrchestrationException.cs new file mode 100644 index 0000000..b148b1d --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/FailedPdsOrchestrationException.cs @@ -0,0 +1,17 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions +{ + public class FailedPdsOrchestrationException : Xeption + { + public FailedPdsOrchestrationException(string message, Exception innerException, IDictionary data) + : base(message, innerException, data) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/InvalidArgumentPdsOrchestrationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/InvalidArgumentPdsOrchestrationException.cs new file mode 100644 index 0000000..955b4eb --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/InvalidArgumentPdsOrchestrationException.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions +{ + public class InvalidArgumentPdsOrchestrationException : Xeption + { + public InvalidArgumentPdsOrchestrationException(string message) + : base(message) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationDependencyException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationDependencyException.cs new file mode 100644 index 0000000..26eca91 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationDependencyException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions +{ + public class PdsOrchestrationDependencyException : Xeption + { + public PdsOrchestrationDependencyException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationDependencyValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationDependencyValidationException.cs new file mode 100644 index 0000000..fec931a --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationDependencyValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions +{ + public class PdsOrchestrationDependencyValidationException : Xeption + { + public PdsOrchestrationDependencyValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationServiceException.cs new file mode 100644 index 0000000..5c77764 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationServiceException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions +{ + public class PdsOrchestrationServiceException : Xeption + { + public PdsOrchestrationServiceException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationValidationException.cs new file mode 100644 index 0000000..ebd8193 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/PdsOrchestrationValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions +{ + public class PdsOrchestrationValidationException : Xeption + { + public PdsOrchestrationValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/UnauthorizedPdsOrchestrationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/UnauthorizedPdsOrchestrationException.cs new file mode 100644 index 0000000..18c5bff --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Orchestrations/Pds/Exceptions/UnauthorizedPdsOrchestrationException.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions +{ + public class UnauthorizedPdsOrchestrationException : Xeption + { + public UnauthorizedPdsOrchestrationException(string message) + : base(message) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingDependencyException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingDependencyException.cs new file mode 100644 index 0000000..a15a11a --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingDependencyException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions +{ + public class CareIdentityServiceProcessingDependencyException : Xeption + { + public CareIdentityServiceProcessingDependencyException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingDependencyValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingDependencyValidationException.cs new file mode 100644 index 0000000..236c6d6 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingDependencyValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions +{ + public class CareIdentityServiceProcessingDependencyValidationException : Xeption + { + public CareIdentityServiceProcessingDependencyValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingServiceException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingServiceException.cs new file mode 100644 index 0000000..f3883e3 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingServiceException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions +{ + public class CareIdentityServiceProcessingServiceException : Xeption + { + public CareIdentityServiceProcessingServiceException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingValidationException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingValidationException.cs new file mode 100644 index 0000000..d5ca879 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/CareIdentityServiceProcessingValidationException.cs @@ -0,0 +1,16 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions +{ + public class CareIdentityServiceProcessingValidationException : Xeption + { + public CareIdentityServiceProcessingValidationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/FailedCareIdentityServiceProcessingException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/FailedCareIdentityServiceProcessingException.cs new file mode 100644 index 0000000..ab3f981 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/FailedCareIdentityServiceProcessingException.cs @@ -0,0 +1,17 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions +{ + public class FailedCareIdentityServiceProcessingException : Xeption + { + public FailedCareIdentityServiceProcessingException(string message, Exception innerException, IDictionary data) + : base(message, innerException, data) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/InvalidArgumentCareIdentityServiceProcessingException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/InvalidArgumentCareIdentityServiceProcessingException.cs new file mode 100644 index 0000000..8d7d115 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/InvalidArgumentCareIdentityServiceProcessingException.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions +{ + public class InvalidArgumentCareIdentityServiceProcessingException : Xeption + { + public InvalidArgumentCareIdentityServiceProcessingException(string message) + : base(message) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/UnauthorisedCareIdentityServiceProcessingException.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/UnauthorisedCareIdentityServiceProcessingException.cs new file mode 100644 index 0000000..cfa3a72 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Processings/CareIdentityServices/Exceptions/UnauthorisedCareIdentityServiceProcessingException.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions +{ + public class UnauthorisedCareIdentityServiceProcessingException : Xeption + { + public UnauthorisedCareIdentityServiceProcessingException(string message) + : base(message) + { } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj b/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj new file mode 100644 index 0000000..f29926d --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj @@ -0,0 +1,65 @@ + + + + net10.0 + disable + disable + + NHS.Digital.ApiPlatform.Sdk + NHS.Digital.ApiPlatform.Sdk + NHS.Digital.ApiPlatform.Sdk + North East London ICB + North East London ICB + + NHS Digital API Platform Client. + + North East London ICB - 2026 (c) + NhsDigitalIcon.png + https://github.com/NHSISL/NHS.Digital.ApiPlatform + https://github.com/NHSISL/NHS.Digital.ApiPlatform + git + NHSISL; NHS Digital; API; Platform; SDK; Client; .NET; The Standard; + + Initial release of the NHS Digital API Platform Client. + + True + 0.1.0.0 + 0.1.0.0 + 0.1.0.0 + README.md + LICENSE.txt + true + True + CS1998 + + + + + true + + Always + + + True + + + + True + + + + + + + + + + + + + + + + + + diff --git a/NHS.Digital.ApiPlatform.Sdk/README.md b/NHS.Digital.ApiPlatform.Sdk/README.md new file mode 100644 index 0000000..0e5383f --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/README.md @@ -0,0 +1,209 @@ +# NHS.Digital.ApiPlatform.Sdk (Core) + +## Overview + +`NHS.Digital.ApiPlatform.Sdk` is a host-agnostic .NET SDK that wraps the NHS Digital API Platform: + +Currently support includes: +- CIS2 Authentication (Authorization Code Flow) +- PDS FHIR R4 client (example implementation) + +(Future extensibility for additional NHS Digital APIs is planned.) + +This package does **not** depend on ASP.NET Core. +You must provide implementations of token and state storage interfaces, +allowing the SDK to work in any .NET host environment. + +--- + +## Installation + +```bash +dotnet add package NHS.Digital.ApiPlatform.Sdk +``` + +--- + +## Configuration + +The SDK is configured via `ApiPlatformConfigurations`: + +```csharp +using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; + +var config = new ApiPlatformConfigurations +{ + CareIdentity = new CareIdentityConfigurations + { + ClientId = "...", + ClientSecret = "...", + RedirectUri = "...", + AuthEndpoint = "...", + TokenEndpoint = "...", + UserInfoEndpoint = "...", + AcrValues = "aal3" // optional + }, + PersonalDemographicsService = new PersonalDemographicsServiceConfigurations + { + BaseUrl = "https://.../personal-demographics/FHIR/R4" + } +}; +``` + +--- + +## Storage Abstractions + +The SDK relies on two storage abstractions: + +- `IApiPlatformStateBroker` (CSRF state for the login flow) +- `IApiPlatformTokenBroker` (access/refresh tokens and expiry timestamps) + +### Default Implementations (In-Memory) + +The Core SDK includes optional in-memory implementations: + +- `MemoryApiPlatformStateBroker` +- `MemoryApiPlatformTokenBroker` + +These are suitable for: + +- Development +- Prototypes +- Console applications / single-user processes + +For production web applications, prefer a host-appropriate implementation (e.g., session, distributed cache, or database-backed storage). The ASP.NET Core package provides web-specific implementations. + +--- + +## 🚀 Quick Start - Registration (DI) + +Register the core services: + +```csharp +using NHS.Digital.ApiPlatform.Sdk; + +services.AddApiPlatformSdkCore(config); +``` + +If you are running outside ASP.NET Core and want the in-memory defaults: + +```csharp +services.AddApiPlatformSdkCore(config); +services.AddApiPlatformSdkInMemoryStorage(); +``` + +### Production storage (non in-memory) +If you do not use the in-memory defaults, register your own implementations of: + +- IApiPlatformStateBroker +- IApiPlatformTokenBroker + +Example: register custom production-ready brokers (database, distributed cache, key vault, etc.): + +```csharp +using NHS.Digital.ApiPlatform.Sdk; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; + +// Your implementations (examples) +services.AddSingleton(); +services.AddSingleton(); + +services.AddApiPlatformSdkCore(config); +``` + +> ASP.NET Core applications should use the `NHS.Digital.ApiPlatform.Sdk.AspNetCore` package to register web-specific storage. [NHS.Digital.ApiPlatform.Sdk.AspNetCore README](../NHS.Digital.ApiPlatform.Sdk.AspNetCore/README.md) + +--- + +## 🚀 Quick Start (No DI) + +For simple hosts, you can instantiate the root client directly: + +```csharp +using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; + +IApiPlatformClient apiPlatformClient = new ApiPlatformClient(config); +``` + +This constructor wires up the SDK internally and uses in-memory storage defaults. + +### Using Custom Storage (No DI) + +If you want to provide production-ready storage implementations +(for example, database-backed or distributed cache), use the static Create method: + +```csharp +using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; + +IApiPlatformStateBroker stateBroker = new MyProductionStateBroker(); +IApiPlatformTokenBroker tokenBroker = new MyProductionTokenBroker(); + +IApiPlatformClient apiPlatformClient = + ApiPlatformClient.Create(config, stateBroker, tokenBroker); +``` + ApiPlatformClient.Create(config, stateBroker, tokenBroker); + +If either broker is omitted (or passed as null), +the SDK will automatically fall back to the built-in in-memory implementations. + +> The Create method is intended for non-DI scenarios. +ASP.NET Core applications should use the DI registration approach instead. [NHS.Digital.ApiPlatform.Sdk.AspNetCore README](../NHS.Digital.ApiPlatform.Sdk.AspNetCore/README.md) +--- + +## Using the SDK + +### Start Login + +```csharp +string loginUrl = await apiPlatformClient + .CareIdentityServiceClient + .BuildLoginUrlAsync(cancellationToken); + +return Redirect(loginUrl); +``` + +### Handle Callback and Retrieve User Info + +Use the processing-based convenience method which completes the callback flow and returns user information: + +```csharp +var userInfo = await apiPlatformClient + .CareIdentityServiceClient + .GetUserInfoAsync(code, state, cancellationToken); +``` + +This call: + +1. Validates `code` and `state` +2. Exchanges the authorization code for tokens +3. Stores access and refresh tokens +4. Retrieves user information + +### Retrieve an Access Token (Auto Refresh Enabled) + +```csharp +string accessToken = await apiPlatformClient + .CareIdentityServiceClient + .GetAccessTokenAsync(cancellationToken); +``` + +If the access token is expired or expiring within the next 60 seconds, the SDK will refresh it using the refresh token, store the new tokens, and return the refreshed access token. + +### Search Patients + +```csharp +string responseJson = await apiPlatformClient + .PersonalDemographicsServiceClient + .SearchPatientsAsync( + family: "Smith", + given: new[] { "John" }, + gender: "male", + birthdate: new DateOnly(1980, 1, 1), + cancellationToken: cancellationToken); +``` + +--- + +© North East London ICB diff --git a/NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs b/NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..253b1b9 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs @@ -0,0 +1,51 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Cryptographies; +using NHS.Digital.ApiPlatform.Sdk.Brokers.DateTimes; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Https; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Serializations; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; +using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; +using NHS.Digital.ApiPlatform.Sdk.Clients.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices; +using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; +using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Services.Processings.CareIdentityServices; + +namespace NHS.Digital.ApiPlatform.Sdk +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddApiPlatformSdkCore( + this IServiceCollection services, + ApiPlatformConfigurations apiPlatformConfigurations) + { + services.AddSingleton(apiPlatformConfigurations); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHttpClient("NhsApiPlatform"); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.TryAddTransient(); + + return services; + } + + public static IServiceCollection AddApiPlatformSdkInMemoryStorage(this IServiceCollection services) + { + // Defaults for standalone. ASP.NET Core package will NOT call this. + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } + } +} \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.Exceptions.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.Exceptions.cs new file mode 100644 index 0000000..634dd12 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.Exceptions.cs @@ -0,0 +1,89 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices +{ + internal partial class CareIdentityService + { + private delegate ValueTask ReturningTaskFunction(); + private delegate ValueTask ReturningNothingFunction(); + + private async ValueTask TryCatch(ReturningTaskFunction returningTaskFunction) + { + try + { + return await returningTaskFunction(); + } + catch (InvalidArgumentCareIdentityServiceException invalidArgumentCareIdentityServiceException) + { + throw await CreateValidationExceptionAsync(invalidArgumentCareIdentityServiceException); + } + catch (UnauthorisedCareIdentityServiceException unauthorisedCareIdentityServiceException) + { + throw await CreateValidationExceptionAsync(unauthorisedCareIdentityServiceException); + } + catch (Exception exception) + { + var failedPatientServiceException = + new FailedCareIdentityServiceException( + message: "Failed care identity service error occurred, please contact support.", + innerException: exception, + data: exception.Data); + + throw await CreateServiceExceptionAsync(failedPatientServiceException); + } + } + + private async ValueTask TryCatch(ReturningNothingFunction returningNothingFunction) + { + try + { + await returningNothingFunction(); + } + catch (InvalidArgumentCareIdentityServiceException invalidArgumentCareIdentityServiceException) + { + throw await CreateValidationExceptionAsync(invalidArgumentCareIdentityServiceException); + } + catch (UnauthorisedCareIdentityServiceException unauthorisedCareIdentityServiceException) + { + throw await CreateValidationExceptionAsync(unauthorisedCareIdentityServiceException); + } + catch (Exception exception) + { + var failedPatientServiceException = + new FailedCareIdentityServiceException( + message: "Failed care identity service error occurred, please contact support.", + innerException: exception, + data: exception.Data); + + throw await CreateServiceExceptionAsync(failedPatientServiceException); + } + } + + private async ValueTask CreateValidationExceptionAsync( + Xeption exception) + { + var careIdentityServiceValidationException = new CareIdentityServiceValidationException( + message: "Care identity service validation error occurred, please fix the errors and try again.", + innerException: exception); + + return careIdentityServiceValidationException; + } + + private async ValueTask CreateServiceExceptionAsync( + Xeption exception) + { + var careIdentityServiceServiceException = new CareIdentityServiceServiceException( + message: "Care identity service error occurred, please contact support.", + innerException: exception); + + return careIdentityServiceServiceException; + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.Validations.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.Validations.cs new file mode 100644 index 0000000..58a66df --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.Validations.cs @@ -0,0 +1,84 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices +{ + internal partial class CareIdentityService + { + public void ValidateOnCallback(string code, string state) + { + Validate( + createException: () => new InvalidArgumentCareIdentityServiceException( + message: "Invalid argument(s), please correct the errors and try again."), + + (Rule: IsInvalid(code), Parameter: nameof(code)), + (Rule: IsInvalid(state), Parameter: nameof(state))); + } + + public void ValidateOnExchangeCodeForToken(string code) + { + Validate( + createException: () => new InvalidArgumentCareIdentityServiceException( + message: "Invalid argument(s), please correct the errors and try again."), + + (Rule: IsInvalid(code), Parameter: nameof(code))); + } + + public void ValidateAccessToken(string accessToken) + { + if (string.IsNullOrWhiteSpace(accessToken)) + { + throw new UnauthorisedCareIdentityServiceException( + message: "Authentication failed (no access token)."); + } + } + + public void ValidateOnGetUserInfo(string accessToken) + { + Validate( + createException: () => new InvalidArgumentCareIdentityServiceException( + message: "Invalid argument(s), please correct the errors and try again."), + + (Rule: IsInvalid(accessToken), Parameter: nameof(accessToken))); + } + + public void ValidateOnExchangeRefreshTokenForToken(string refreshToken) + { + Validate( + createException: () => new InvalidArgumentCareIdentityServiceException( + message: "Invalid argument(s), please correct the errors and try again."), + + (Rule: IsInvalid(refreshToken), Parameter: nameof(refreshToken))); + } + + private static dynamic IsInvalid(string? text) => new + { + Condition = string.IsNullOrWhiteSpace(text), + Message = "Text is required" + }; + + private static void Validate( + Func createException, + params (dynamic Rule, string Parameter)[] validations) + where T : Xeption + { + T invalidDataException = createException(); + + foreach ((dynamic rule, string parameter) in validations) + { + if (rule.Condition) + { + invalidDataException.UpsertDataList( + key: parameter, + value: rule.Message); + } + } + + invalidDataException.ThrowIfContainsErrors(); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.cs new file mode 100644 index 0000000..5c3ed2b --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/CareIdentityService.cs @@ -0,0 +1,240 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Cryptographies; +using NHS.Digital.ApiPlatform.Sdk.Brokers.DateTimes; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Https; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Serializations; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; +using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices +{ + internal sealed partial class CareIdentityService : ICareIdentityService + { + private readonly ApiPlatformConfigurations configurations; + private readonly IHttpBroker httpBroker; + private readonly IJsonBroker jsonBroker; + private readonly ICryptoBroker cryptoBroker; + private readonly IDateTimeBroker dateTimeBroker; + private readonly IApiPlatformStateBroker stateBroker; + private readonly IApiPlatformTokenBroker tokenBroker; + + public CareIdentityService( + ApiPlatformConfigurations configurations, + IHttpBroker httpBroker, + IJsonBroker jsonBroker, + ICryptoBroker cryptoBroker, + IDateTimeBroker dateTimeBroker, + IApiPlatformStateBroker stateBroker, + IApiPlatformTokenBroker tokenBroker) + { + this.configurations = configurations; + this.httpBroker = httpBroker; + this.jsonBroker = jsonBroker; + this.cryptoBroker = cryptoBroker; + this.dateTimeBroker = dateTimeBroker; + this.stateBroker = stateBroker; + this.tokenBroker = tokenBroker; + } + public ValueTask BuildLoginUrlAsync(CancellationToken cancellationToken = default) => + TryCatch(async () => + { + string csrfState = this.cryptoBroker.CreateUrlSafeState(); + await this.stateBroker.StoreCsrfStateAsync(csrfState, cancellationToken); + CareIdentityConfigurations careIdentityConfigurations = this.configurations.CareIdentity; + + // CIS2 only supports these parameters (no PKCE) + string url = + $"{careIdentityConfigurations.AuthEndpoint}" + + $"?client_id={careIdentityConfigurations.ClientId}" + + $"&redirect_uri={Uri.EscapeDataString(careIdentityConfigurations.RedirectUri)}" + + $"&response_type=code" + + $"&state={csrfState}"; + + if (string.IsNullOrWhiteSpace(careIdentityConfigurations.AcrValues) is false) + { + url += $"&acr_values={careIdentityConfigurations.AcrValues}"; + } + + return url; + }); + + public ValueTask LogoutAsync(CancellationToken cancellationToken = default) => + TryCatch(async () => + { + await this.stateBroker.ClearCsrfStateAsync(cancellationToken); + await this.tokenBroker.ClearAccessTokenAsync(cancellationToken); + await this.tokenBroker.ClearRefreshTokenAsync(cancellationToken); + }); + + public ValueTask CallbackAsync( + string code, + string state, + CancellationToken cancellationToken = default) => + TryCatch(async () => + { + ValidateOnCallback(code, state); + + string? expectedState = await this.stateBroker.GetCsrfStateAsync(cancellationToken); + + if (string.IsNullOrWhiteSpace(expectedState) || + string.Equals(state, expectedState, StringComparison.Ordinal) is false) + { + throw new InvalidOperationException("Invalid state parameter."); + } + + await this.stateBroker.ClearCsrfStateAsync(cancellationToken); + + TokenResult token = await ExchangeCodeForTokenAsync(code, cancellationToken); + _ = await GetUserInfoAsync(token.AccessToken, cancellationToken); + _ = int.TryParse(token.ExpiresIn, out int accessExpiresInSeconds); + _ = int.TryParse(token.RefreshTokenExpiresIn, out int refreshExpiresInSeconds); + + DateTimeOffset now = this.dateTimeBroker.GetCurrentDateTimeOffset(); + DateTimeOffset accessExpiresAtUtc = now.AddSeconds(Math.Max(accessExpiresInSeconds, 0)); + DateTimeOffset refreshExpiresAtUtc = now.AddSeconds(Math.Max(refreshExpiresInSeconds, 0)); + + await this.tokenBroker.StoreAccessTokenAsync(token.AccessToken, accessExpiresAtUtc, cancellationToken); + + if (string.IsNullOrWhiteSpace(token.RefreshToken) is false) + { + await this.tokenBroker.StoreRefreshTokenAsync( + token.RefreshToken, + refreshExpiresAtUtc, + cancellationToken); + } + }); + + public ValueTask GetAccessTokenAsync(CancellationToken cancellationToken = default) => + TryCatch(async () => + { + var (accessToken, accessExpiresAtUtc) = + await this.tokenBroker.GetAccessTokenAsync(cancellationToken); + + DateTimeOffset now = this.dateTimeBroker.GetCurrentDateTimeOffset(); + + if (string.IsNullOrWhiteSpace(accessToken) is false && + accessExpiresAtUtc is not null && + accessExpiresAtUtc.Value > now.AddSeconds(60)) + { + return accessToken!; + } + + var (refreshToken, refreshExpiresAtUtc) = + await this.tokenBroker.GetRefreshTokenAsync(cancellationToken); + + if (string.IsNullOrWhiteSpace(refreshToken) || + refreshExpiresAtUtc is null || + refreshExpiresAtUtc.Value <= now) + { + return string.Empty; + } + + TokenResult refreshed = + await ExchangeRefreshTokenForTokenAsync(refreshToken!, cancellationToken); + + _ = int.TryParse(refreshed.ExpiresIn, out int newAccessExpiresInSeconds); + _ = int.TryParse(refreshed.RefreshTokenExpiresIn, out int newRefreshExpiresInSeconds); + + DateTimeOffset newAccessExpiresAtUtc = now.AddSeconds(Math.Max(newAccessExpiresInSeconds, 0)); + DateTimeOffset newRefreshExpiresAtUtc = now.AddSeconds(Math.Max(newRefreshExpiresInSeconds, 0)); + + await this.tokenBroker.StoreAccessTokenAsync( + refreshed.AccessToken, + newAccessExpiresAtUtc, + cancellationToken); + + if (string.IsNullOrWhiteSpace(refreshed.RefreshToken) is false) + { + await this.tokenBroker.StoreRefreshTokenAsync( + refreshed.RefreshToken, + newRefreshExpiresAtUtc, + cancellationToken); + } + + ValidateAccessToken(refreshed.AccessToken); + + return refreshed.AccessToken; + }); + + public ValueTask GetUserInfoAsync(string accessToken, CancellationToken cancellationToken) => + TryCatch(async () => + { + ValidateOnGetUserInfo(accessToken); + CareIdentityConfigurations careIdentityConfigurations = this.configurations.CareIdentity; + + var response = await this.httpBroker.GetAsync( + careIdentityConfigurations.UserInfoEndpoint, + request => request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken), + cancellationToken); + + response.EnsureSuccessStatusCode(); + + string json = await response.Content.ReadAsStringAsync(cancellationToken); + NhsUserInfo? userInfo = this.jsonBroker.Deserialize(json); + + return userInfo ?? throw new InvalidOperationException("UserInfo endpoint returned an invalid payload."); + }); + + private ValueTask ExchangeCodeForTokenAsync(string code, CancellationToken cancellationToken) => + TryCatch(async () => + { + ValidateOnExchangeCodeForToken(code); + CareIdentityConfigurations careIdentityConfigurations = this.configurations.CareIdentity; + + var formValues = new[] + { + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("code", code), + new KeyValuePair("redirect_uri", careIdentityConfigurations.RedirectUri), + new KeyValuePair("client_id", careIdentityConfigurations.ClientId), + new KeyValuePair("client_secret", careIdentityConfigurations.ClientSecret) + }; + + var response = await this.httpBroker + .PostFormAsync(careIdentityConfigurations.TokenEndpoint, formValues, cancellationToken); + + response.EnsureSuccessStatusCode(); + + string json = await response.Content.ReadAsStringAsync(cancellationToken); + TokenResult? token = this.jsonBroker.Deserialize(json); + + return token ?? throw new InvalidOperationException("Token endpoint returned an invalid payload."); + }); + + private ValueTask ExchangeRefreshTokenForTokenAsync( + string refreshToken, + CancellationToken cancellationToken) => + TryCatch(async () => + { + ValidateOnExchangeRefreshTokenForToken(refreshToken); + + CareIdentityConfigurations careIdentityConfigurations = this.configurations.CareIdentity; + + var formValues = new[] + { + new KeyValuePair("grant_type", "refresh_token"), + new KeyValuePair("refresh_token", refreshToken), + new KeyValuePair("client_id", careIdentityConfigurations.ClientId), + new KeyValuePair("client_secret", careIdentityConfigurations.ClientSecret) + }; + + var response = await this.httpBroker + .PostFormAsync(careIdentityConfigurations.TokenEndpoint, formValues, cancellationToken); + + response.EnsureSuccessStatusCode(); + + string json = await response.Content.ReadAsStringAsync(cancellationToken); + TokenResult? token = this.jsonBroker.Deserialize(json); + + return token ?? throw new InvalidOperationException("Token endpoint returned an invalid payload."); + }); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/ICareIdentityService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/ICareIdentityService.cs new file mode 100644 index 0000000..fa1117a --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/CareIdentityServices/ICareIdentityService.cs @@ -0,0 +1,18 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System.Threading; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices +{ + public interface ICareIdentityService + { + ValueTask BuildLoginUrlAsync(CancellationToken cancellationToken = default); + ValueTask LogoutAsync(CancellationToken cancellationToken = default); + ValueTask CallbackAsync(string code, string state, CancellationToken cancellationToken = default); + ValueTask GetAccessTokenAsync(CancellationToken cancellationToken = default); + ValueTask GetUserInfoAsync(string accessToken, CancellationToken cancellationToken = default); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs new file mode 100644 index 0000000..83d8ca7 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs @@ -0,0 +1,22 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds +{ + internal interface IPdsService + { + ValueTask SearchPatientsAsync( + string accessToken, + string family, + IEnumerable? given = null, + string? gender = null, + DateOnly? birthdate = null, + CancellationToken cancellationToken = default); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.Exceptions.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.Exceptions.cs new file mode 100644 index 0000000..ea0e8cd --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.Exceptions.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds +{ + internal partial class PdsService : IPdsService + { + private delegate ValueTask ReturningStringFunction(); + + private async ValueTask TryCatch(ReturningStringFunction returningStringFunction) + { + try + { + return await returningStringFunction(); + } + catch (InvalidArgumentPdsServiceException invalidArgumentPdsServiceException) + { + throw await CreateValidationExceptionAsync(invalidArgumentPdsServiceException); + } + //TODO: Extend this to catch dependency and dependency validation exceptions. + catch (Exception exception) + { + var failedPdsServiceException = + new FailedPdsServiceException( + message: "Failed PDS service error occurred, please contact support.", + innerException: exception, + data: exception.Data); + + throw await CreateServiceExceptionAsync(failedPdsServiceException); + } + } + + private async ValueTask CreateValidationExceptionAsync( + Xeption exception) + { + var pdsServiceValidationException = new PdsServiceValidationException( + message: "PDS service validation error occurred, please fix the errors and try again.", + innerException: exception); + + return pdsServiceValidationException; + } + + private async ValueTask CreateServiceExceptionAsync(Xeption exception) + { + var pdsServiceException = new PdsServiceException( + message: "PDS service error occurred, please contact support.", + innerException: exception); + + return pdsServiceException; + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.Validations.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.Validations.cs new file mode 100644 index 0000000..3a1f675 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.Validations.cs @@ -0,0 +1,72 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds +{ + internal partial class PdsService : IPdsService + { + public void ValidateOnSearchPatientsAsync( + string accessToken, + string family, + IEnumerable given, + string gender, + DateOnly? birthdate) + { + Validate( + createException: () => new InvalidArgumentPdsServiceException( + message: "Invalid argument(s), please correct the errors and try again."), + + (Rule: IsInvalid(family), Parameter: nameof(family)), + (Rule: IsInvalid(given), Parameter: nameof(given)), + (Rule: IsInvalid(gender), Parameter: nameof(gender)), + (Rule: IsInvalid(birthdate), Parameter: nameof(birthdate))); + } + + private static dynamic IsInvalid(string? text) => new + { + Condition = string.IsNullOrWhiteSpace(text), + Message = "Text is required" + }; + + private static dynamic IsInvalid(DateOnly? dateOnly) => new + { + Condition = dateOnly == null, + Message = "Date is required" + }; + + private static dynamic IsInvalid(IEnumerable textList) => new + { + Condition = textList != null && + textList.Any(text => string.IsNullOrWhiteSpace(text)), + + Message = "List contains null or whitespace values" + }; + + private static void Validate( + Func createException, + params (dynamic Rule, string Parameter)[] validations) + where T : Xeption + { + T invalidDataException = createException(); + + foreach ((dynamic rule, string parameter) in validations) + { + if (rule.Condition) + { + invalidDataException.UpsertDataList( + key: parameter, + value: rule.Message); + } + } + + invalidDataException.ThrowIfContainsErrors(); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs new file mode 100644 index 0000000..65a29c9 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs @@ -0,0 +1,79 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Https; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Identifiers; +using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds +{ + internal partial class PdsService : IPdsService + { + private readonly ApiPlatformConfigurations configurations; + private readonly IHttpBroker httpBroker; + private readonly IIdentifierBroker identifierBroker; + + public PdsService( + IOptions configurations, + IHttpBroker httpBroker, + IIdentifierBroker identifierBroker) + { + this.configurations = configurations.Value; + this.httpBroker = httpBroker; + this.identifierBroker = identifierBroker; + } + + public ValueTask SearchPatientsAsync( + string accessToken, + string family, + IEnumerable given = null, + string gender = null, + DateOnly? birthdate = null, + CancellationToken cancellationToken = default) => + TryCatch(async () => + { + string baseUrl = this.configurations.PersonalDemographicsService.BaseUrl.TrimEnd('/'); + string url = $"{baseUrl}/Patient?family={Uri.EscapeDataString(family)}"; + + if (given is not null) + { + foreach (string givenName in given.Where(n => !string.IsNullOrWhiteSpace(n))) + { + url += $"&given={Uri.EscapeDataString(givenName)}"; + } + } + + if (string.IsNullOrWhiteSpace(gender) is false) + { + url += $"&gender={Uri.EscapeDataString(gender)}"; + } + + if (birthdate is not null) + { + url += $"&birthdate=eq{birthdate:yyyy-MM-dd}"; + } + + var response = await this.httpBroker.GetAsync( + url, + request => + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + request.Headers.Add("X-Request-ID", this.identifierBroker.GetNewGuid().ToString()); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/fhir+json")); + }, + cancellationToken); + + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(cancellationToken); + }); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs new file mode 100644 index 0000000..1e3d75f --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs @@ -0,0 +1,20 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds +{ + public interface IPdsOrchestrationService + { + ValueTask SearchPatientsAsync( + string family, + IEnumerable? given = null, + string? gender = null, + DateOnly? birthdate = null, + CancellationToken cancellationToken = default); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.Exceptions.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.Exceptions.cs new file mode 100644 index 0000000..5922b58 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.Exceptions.cs @@ -0,0 +1,115 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds.Exceptions; +using NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds +{ + internal sealed partial class PdsOrchestrationService + { + private delegate ValueTask ReturningStringFunction(); + + private async ValueTask TryCatch(ReturningStringFunction returningStringFunction) + { + try + { + return await returningStringFunction(); + } + catch (InvalidArgumentPdsOrchestrationException invalidArgumentPdsOrchestrationException) + { + throw await CreateValidationExceptionAsync(invalidArgumentPdsOrchestrationException); + } + catch (UnauthorizedPdsOrchestrationException unauthorizedPdsOrchestrationException) + { + throw await CreateValidationExceptionAsync(unauthorizedPdsOrchestrationException); + } + catch (CareIdentityServiceValidationException careIdentityValidationException) + { + throw await CreateDependencyValidationExceptionAsync(careIdentityValidationException); + } + catch (CareIdentityServiceDependencyValidationException careIdentityDependencyValidationException) + { + throw await CreateDependencyValidationExceptionAsync(careIdentityDependencyValidationException); + } + catch (CareIdentityServiceDependencyException careIdentityServiceDependencyException) + { + throw await CreateDependencyExceptionAsync(careIdentityServiceDependencyException); + } + catch (CareIdentityServiceServiceException careIdentityServiceServiceException) + { + throw await CreateDependencyExceptionAsync(careIdentityServiceServiceException); + } + catch (PdsServiceValidationException pdsIdentityValidationException) + { + throw await CreateDependencyValidationExceptionAsync(pdsIdentityValidationException); + } + catch (PdsServiceDependencyValidationException pdsIdentityDependencyValidationException) + { + throw await CreateDependencyValidationExceptionAsync(pdsIdentityDependencyValidationException); + } + catch (PdsServiceDependencyException pdsIdentityServiceDependencyException) + { + throw await CreateDependencyExceptionAsync(pdsIdentityServiceDependencyException); + } + catch (PdsServiceException pdsIdentityServiceServiceException) + { + throw await CreateDependencyExceptionAsync(pdsIdentityServiceServiceException); + } + catch (Exception exception) + { + var failedPdsOrchestrationException = + new FailedPdsOrchestrationException( + message: "Failed PDS orchestration service error occurred, please contact support.", + innerException: exception, + data: exception.Data); + + throw await CreateServiceExceptionAsync(failedPdsOrchestrationException); + } + } + + private async ValueTask CreateValidationExceptionAsync(Xeption exception) + { + var pdsOrchestrationValidationException = + new PdsOrchestrationValidationException( + message: "PDS orchestration validation error occurred, fix the errors and try again.", + innerException: exception); + + return pdsOrchestrationValidationException; + } + + private async ValueTask CreateDependencyValidationExceptionAsync( + Xeption exception) + { + var pdsOrchestrationDependencyValidationException = + new PdsOrchestrationDependencyValidationException( + message: "PDS orchestration dependency validation error occurred, fix the errors and try again.", + innerException: exception.InnerException as Xeption); + + return pdsOrchestrationDependencyValidationException; + } + + private async ValueTask CreateDependencyExceptionAsync(Xeption exception) + { + var pdsOrchestrationDependencyException = + new PdsOrchestrationDependencyException( + message: "PDS orchestration dependency error occurred, fix the errors and try again.", + innerException: exception.InnerException as Xeption); + + return pdsOrchestrationDependencyException; + } + + private async ValueTask CreateServiceExceptionAsync(Xeption exception) + { + var pdsOrchestrationServiceException = new PdsOrchestrationServiceException( + message: "PDS orchestration service error occurred, please contact support.", + innerException: exception); + + return pdsOrchestrationServiceException; + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.Validations.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.Validations.cs new file mode 100644 index 0000000..446cbc0 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.Validations.cs @@ -0,0 +1,70 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Linq; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices.Exceptions; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds +{ + internal sealed partial class PdsOrchestrationService + { + public void ValidateOnSearchPatientsAsync( + string family, + IEnumerable given, + string gender, + DateOnly? birthdate) + { + Validate( + createException: () => new InvalidArgumentCareIdentityServiceException( + message: "Invalid argument(s), please correct the errors and try again."), + + (Rule: IsInvalid(family), Parameter: nameof(family)), + (Rule: IsInvalid(given), Parameter: nameof(given)), + (Rule: IsInvalid(gender), Parameter: nameof(gender)), + (Rule: IsInvalid(birthdate), Parameter: nameof(birthdate))); + } + + private static dynamic IsInvalid(string? text) => new + { + Condition = string.IsNullOrWhiteSpace(text), + Message = "Text is required" + }; + + private static dynamic IsInvalid(DateOnly? dateOnly) => new + { + Condition = dateOnly == null, + Message = "Date is required" + }; + + private static dynamic IsInvalid(IEnumerable textList) => new + { + Condition = textList != null && + textList.Any(text => string.IsNullOrWhiteSpace(text)), + + Message = "List contains null or whitespace values" + }; + + private static void Validate( + Func createException, + params (dynamic Rule, string Parameter)[] validations) + where T : Xeption + { + T invalidDataException = createException(); + + foreach ((dynamic rule, string parameter) in validations) + { + if (rule.Condition) + { + invalidDataException.UpsertDataList( + key: parameter, + value: rule.Message); + } + } + + invalidDataException.ThrowIfContainsErrors(); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs new file mode 100644 index 0000000..1559aa9 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs @@ -0,0 +1,48 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; +using NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions; +using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds +{ + internal sealed partial class PdsOrchestrationService : IPdsOrchestrationService + { + private readonly ICareIdentityService careIdentityService; + private readonly IPdsService pdsService; + private readonly IApiPlatformTokenBroker tokenBroker; + + public PdsOrchestrationService(ICareIdentityService careIdentityService, IPdsService pdsService, IApiPlatformTokenBroker tokenBroker) + { + this.careIdentityService = careIdentityService; + this.pdsService = pdsService; + this.tokenBroker = tokenBroker; + } + + public ValueTask SearchPatientsAsync( + string family, + IEnumerable given = null, + string gender = null, + DateOnly? birthdate = null, + CancellationToken cancellationToken = default) => + TryCatch(async () => + { + ValidateOnSearchPatientsAsync(family, given, gender, birthdate); + string accessToken = await this.careIdentityService.GetAccessTokenAsync(cancellationToken); + + if (string.IsNullOrWhiteSpace(accessToken)) + { + throw new UnauthorizedPdsOrchestrationException("Unauthorized - Unable to retrieve access token."); + } + + return await this.pdsService + .SearchPatientsAsync(accessToken, family, given, gender, birthdate, cancellationToken); + }); + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.Exceptions.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.Exceptions.cs new file mode 100644 index 0000000..2c7a818 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.Exceptions.cs @@ -0,0 +1,85 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Processings.CareIdentityServices +{ + internal partial class CareIdentityServiceProcessingService : ICareIdentityServiceProcessingService + { + private delegate ValueTask ReturningTaskFunction(); + private delegate ValueTask ReturningNothingFunction(); + + private async ValueTask TryCatch(ReturningTaskFunction returningTaskFunction) + { + try + { + return await returningTaskFunction(); + } + catch (InvalidArgumentCareIdentityServiceProcessingException + invalidArgumentCareIdentityServiceProcessingException) + { + throw await CreateValidationExceptionAsync(invalidArgumentCareIdentityServiceProcessingException); + } + catch (UnauthorisedCareIdentityServiceProcessingException + unauthorisedCareIdentityServiceProcessingException) + { + throw await CreateValidationExceptionAsync(unauthorisedCareIdentityServiceProcessingException); + } + catch (Exception exception) + { + var failedCareIdentityServiceProcessingException = + new FailedCareIdentityServiceProcessingException( + message: "Failed care identity service processing error occurred, please contact support.", + innerException: exception, + data: exception.Data); + + throw await CreateServiceExceptionAsync(failedCareIdentityServiceProcessingException); + } + } + + private async ValueTask TryCatch(ReturningNothingFunction returningNothingFunction) + { + try + { + await returningNothingFunction(); + } + catch (Exception exception) + { + var failedCareIdentityServiceProcessingException = + new FailedCareIdentityServiceProcessingException( + message: "Failed care identity service processing error occurred, please contact support.", + innerException: exception, + data: exception.Data); + + throw await CreateServiceExceptionAsync(failedCareIdentityServiceProcessingException); + } + } + + private async ValueTask CreateValidationExceptionAsync( + Xeption exception) + { + var careIdentityServiceProcessingValidationException = new CareIdentityServiceProcessingValidationException( + message: "Care identity service processing validation error occurred, " + + "please fix the errors and try again.", + + innerException: exception); + + return careIdentityServiceProcessingValidationException; + } + + private async ValueTask CreateServiceExceptionAsync( + Xeption exception) + { + var careIdentityServiceProcessingServiceException = new CareIdentityServiceProcessingServiceException( + message: "Care identity service processing error occurred, please contact support.", + innerException: exception); + + return careIdentityServiceProcessingServiceException; + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.Validations.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.Validations.cs new file mode 100644 index 0000000..177ed7f --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.Validations.cs @@ -0,0 +1,58 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using NHS.Digital.ApiPlatform.Sdk.Models.Processings.CareIdentityServices.Exceptions; +using Xeptions; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Processings.CareIdentityServices +{ + internal partial class CareIdentityServiceProcessingService : ICareIdentityServiceProcessingService + { + public void ValidateOnGetUserInfo(string code, string state) + { + Validate( + createException: () => new InvalidArgumentCareIdentityServiceProcessingException( + message: "Invalid argument(s), please correct the errors and try again."), + + (Rule: IsInvalid(code), Parameter: nameof(code)), + (Rule: IsInvalid(state), Parameter: nameof(state))); + } + + public void ValidateAccessToken(string accessToken) + { + if (string.IsNullOrWhiteSpace(accessToken)) + { + throw new UnauthorisedCareIdentityServiceProcessingException( + message: "Authentication failed (no access token)."); + } + } + + private static dynamic IsInvalid(string? text) => new + { + Condition = string.IsNullOrWhiteSpace(text), + Message = "Text is required" + }; + + private static void Validate( + Func createException, + params (dynamic Rule, string Parameter)[] validations) + where T : Xeption + { + T invalidDataException = createException(); + + foreach ((dynamic rule, string parameter) in validations) + { + if (rule.Condition) + { + invalidDataException.UpsertDataList( + key: parameter, + value: rule.Message); + } + } + + invalidDataException.ThrowIfContainsErrors(); + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.cs new file mode 100644 index 0000000..3e86354 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/CareIdentityServiceProcessingService.cs @@ -0,0 +1,56 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System.Threading; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Processings.CareIdentityServices +{ + internal partial class CareIdentityServiceProcessingService : ICareIdentityServiceProcessingService + { + private readonly ICareIdentityService careIdentityService; + + public CareIdentityServiceProcessingService(ICareIdentityService careIdentityService) => + this.careIdentityService = careIdentityService; + + public ValueTask BuildLoginUrlAsync(CancellationToken cancellationToken = default) => + TryCatch(async () => + { + return await this.careIdentityService.BuildLoginUrlAsync(cancellationToken); + }); + + + public ValueTask LogoutAsync(CancellationToken cancellationToken = default) => + TryCatch(async () => + { + await this.careIdentityService.LogoutAsync(cancellationToken); + }); + + public ValueTask GetAccessTokenAsync(CancellationToken cancellationToken = default) => + TryCatch(async () => + { + return await this.careIdentityService.GetAccessTokenAsync(cancellationToken); + }); + + public ValueTask GetUserInfoAsync( + string code, + string state, + CancellationToken cancellationToken = default) => + TryCatch(async () => + { + ValidateOnGetUserInfo(code, state); + await this.careIdentityService.CallbackAsync(code, state, cancellationToken); + string accessToken = await this.careIdentityService.GetAccessTokenAsync(cancellationToken); + ValidateAccessToken(accessToken); + + NhsUserInfo userInfo = await this.careIdentityService + .GetUserInfoAsync(accessToken, cancellationToken); + + return userInfo; + }); + + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/ICareIdentityServiceProcessingService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/ICareIdentityServiceProcessingService.cs new file mode 100644 index 0000000..30df136 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Processings/CareIdentityServices/ICareIdentityServiceProcessingService.cs @@ -0,0 +1,21 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- +using System.Threading; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Processings.CareIdentityServices +{ + public interface ICareIdentityServiceProcessingService + { + ValueTask BuildLoginUrlAsync(CancellationToken cancellationToken = default); + ValueTask LogoutAsync(CancellationToken cancellationToken = default); + ValueTask GetAccessTokenAsync(CancellationToken cancellationToken = default); + + ValueTask GetUserInfoAsync( + string code, + string state, + CancellationToken cancellationToken = default); + } +} diff --git a/NHS.Digital.ApiPlatform.slnx b/NHS.Digital.ApiPlatform.slnx new file mode 100644 index 0000000..2dcb9b0 --- /dev/null +++ b/NHS.Digital.ApiPlatform.slnx @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index d941c8c..934ec4e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,256 @@ -# NHS.CIS2 -NHS CIS2 Client +# NHS Digital API Platform SDK + +This repository provides a SDK for integrating with the +**NHS Digital API Platform**, including: + +- Care Identity Service (CIS2) Client + - Authentication (Authorization Code Flow) + - Automatic refresh-token renewal + +- Personal Demographics Service (PDS) Client + +(Future extensibility for additional NHS Digital APIs is planned.) + +------------------------------------------------------------------------ + +# 📦 Packages + +This solution is intentionally split into two focused packages: + +- **`NHS.Digital.ApiPlatform.Sdk` (Core)** +- **`NHS.Digital.ApiPlatform.Sdk.AspNetCore` (Web Integration)** + +## 🎯 Why Two Packages? + +The SDK was deliberately designed to be **host-agnostic**. + +The Core package contains: + +- NHS API integration logic +- Authentication and token lifecycle management +- API client implementations +- No dependency on ASP.NET Core + +It does **not** reference: + +- `HttpContext` +- `IHttpContextAccessor` +- `ISession` +- ASP.NET Core middleware +- Web-specific abstractions + +This design ensures the Core SDK can be used in: + +- Console applications +- Background services +- Azure Functions +- Worker services +- Integration pipelines +- Custom web frameworks +- ASP.NET Core (via the integration package) + +--- + +## 🌐 The ASP.NET Core Integration Package + +`NHS.Digital.ApiPlatform.Sdk.AspNetCore` provides: + +- Session-based implementations of: + - `IApiPlatformStateBroker` + - `IApiPlatformTokenBroker` +- `IServiceCollection` extension methods +- ASP.NET Core-specific wiring +- Access to `HttpContext` and session safely + +This package depends on ASP.NET Core abstractions, but the Core SDK does not. + +--- + +## 🧠 Design Rationale + +If the Core SDK directly referenced ASP.NET Core types such as: + +- `IHttpContextAccessor` +- `HttpContext` +- `ISession` + +then: + +- Console applications could not use it. +- Worker services would carry unnecessary web dependencies. +- Unit testing would become more complex. +- The SDK would violate separation-of-concerns principles. + +By splitting responsibilities: + +| Concern | Package | +|---------|----------| +| Authentication logic | Core | +| Token refresh logic | Core | +| HTTP calls to NHS APIs | Core | +| Web session integration | AspNetCore | + +This keeps the architecture: + +- Clean +- Modular +- Testable +- Replaceable +- Environment-agnostic + +------------------------------------------------------------------------ + +## 1️ NHS.Digital.ApiPlatform.Sdk (Core) + +Host-agnostic .NET SDK that: + +This package can be used in: + +- Console applications +- Background services +- Azure Functions +- Custom web frameworks +- ASP.NET (with your own storage implementation) + +👉 **Full documentation:**\ +[NHS.Digital.ApiPlatform.Sdk README](NHS.Digital.ApiPlatform.Sdk/README.md) + +------------------------------------------------------------------------ + +## 2️ NHS.Digital.ApiPlatform.Sdk.AspNetCore + +ASP.NET Core adapter for the core SDK. + +Provides: + +- Session-based storage implementation +- Cookie-based storage implementation (BFF-style) +- ASP.NET DI registration helpers +- Minimal setup for web applications + +This is the recommended package for: + +- ASP.NET Core MVC +- Web APIs +- BFF architectures + +👉 **Full documentation:**\ +[NHS.Digital.ApiPlatform.Sdk.AspNetCore README](NHS.Digital.ApiPlatform.Sdk.AspNetCore/README.md) + +------------------------------------------------------------------------ + +# 🏗 Architecture Overview + +Internally, the SDK follows **The Standard** layering: + +- Brokers +- Foundations +- Validations +- Orchestrations (workflow + auto-refresh logic) +- Exposers (`IApiPlatformClient` surface) + +This ensures: + +- Predictable structure +- High testability +- Clean separation of concerns +- Future extensibility for additional NHS APIs + +------------------------------------------------------------------------ + +# 🔐 Refresh Token Handling + +The SDK automatically: + +1. Detects expired (or near-expiry) access tokens +2. Uses the refresh token +3. Requests new tokens from CIS2 +4. Stores updated tokens +5. Continues execution seamlessly + +No additional developer logic is required. + +------------------------------------------------------------------------ + +# 🧪 Testing + +The solution includes: + +- Unit test projects +- Acceptance test scaffolding +- Integration test scaffolding +- Refresh-token renewal tests + +------------------------------------------------------------------------ + +# 🚀 Getting Started + +## ASP.NET Core Applications + +1. Install the ASP.NET Core integration package: + + `dotnet add package NHS.Digital.ApiPlatform.Sdk.AspNetCore` + + (This package automatically installs the Core SDK as a dependency.) + +2. Configure `ApiPlatform` in `appsettings.json`. + +3. Register the SDK: + + ```cs + services.AddApiPlatformSdkCore(config); + services.AddApiPlatformSdkAspNetCore(); + ``` + +4. Inject and use `IApiPlatformClient` via DI. + + Example: initiating CIS2 login from a controller: + ```cs + using Microsoft.AspNetCore.Mvc; + using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; + + public class AuthController : Controller + { + private readonly IApiPlatformClient apiPlatformClient; + + public AuthController(IApiPlatformClient apiPlatformClient) => + this.apiPlatformClient = apiPlatformClient; + + [HttpGet("login")] + public async Task Login(CancellationToken cancellationToken) + { + string loginUrl = await this.apiPlatformClient + .CareIdentityServiceClient + .BuildLoginUrlAsync(cancellationToken); + + return Redirect(loginUrl); + } + } + ``` + 👉 **Full documentation:**\ + [NHS.Digital.ApiPlatform.Sdk.AspNetCore README](NHS.Digital.ApiPlatform.Sdk.AspNetCore/README.md) + +------------------------------------------------------------------------ + +## Advanced / Non-ASP.NET Hosts + +Install the Core package directly: + + `dotnet add package NHS.Digital.ApiPlatform.Sdk` + +You must implement: + +- `IApiPlatformStateBroker` +- `IApiPlatformTokenBroker` + +Then register: + + `services.AddApiPlatformSdkCore(config);` + + +👉 **Full documentation:**\ +[NHS.Digital.ApiPlatform.Sdk README](NHS.Digital.ApiPlatform.Sdk/README.md) + +------------------------------------------------------------------------ + +© North East London ICB diff --git a/ReactApp1.Server/CHANGELOG.md b/ReactApp1.Server/CHANGELOG.md new file mode 100644 index 0000000..03c2619 --- /dev/null +++ b/ReactApp1.Server/CHANGELOG.md @@ -0,0 +1,8 @@ +This file explains how Visual Studio created the project. + +The following steps were used to generate this project: +- Create new ASP\.NET Core Web API project. +- Update project file to add a reference to the frontend project and set SPA properties. +- Update `launchSettings.json` to register the SPA proxy as a startup assembly. +- Add project to the startup projects list. +- Write this file. diff --git a/ReactApp1.Server/Controllers/AuthController.cs b/ReactApp1.Server/Controllers/AuthController.cs new file mode 100644 index 0000000..57608c2 --- /dev/null +++ b/ReactApp1.Server/Controllers/AuthController.cs @@ -0,0 +1,176 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; +using NHS.Digital.ApiPlatform.Sdk.Models.Clients.CareIdentityService.Exceptions; +using ReactApp1.Server.Data; +using ReactApp1.Server.Models; + +namespace ReactApp1.Server.Controllers; + +[ApiController] +[Route("[controller]")] +public class AuthController : ControllerBase +{ + private readonly IApiPlatformClient apiPlatformClient; + private readonly ILogger logger; + private readonly ApplicationDbContext context; + + public AuthController( + IApiPlatformClient apiPlatformClient, + ILogger logger, + ApplicationDbContext context) + { + this.apiPlatformClient = apiPlatformClient; + this.logger = logger; + this.context = context; + } + + [HttpGet("login")] + public async Task Login(CancellationToken cancellationToken) + { + string url = await this.apiPlatformClient + .CareIdentityServiceClient + .BuildLoginUrlAsync(cancellationToken); + + this.logger.LogInformation("Initiating CIS2 authentication."); + + return Redirect(url); + } + + [Authorize] + [HttpGet("session")] + public async Task Session(CancellationToken cancellationToken) + { + if (User.Identity?.IsAuthenticated is not true) + { + return Unauthorized(); + } + + // Ensure an access token exists for the current session. + string accessToken = await this.apiPlatformClient + .CareIdentityServiceClient + .GetAccessTokenAsync(cancellationToken); + + if (string.IsNullOrWhiteSpace(accessToken)) + { + return Unauthorized(); + } + + return Ok(new + { + sub = User.FindFirstValue(ClaimTypes.NameIdentifier), + upn = User.FindFirstValue(ClaimTypes.Upn) + }); + } + + [Authorize] + [HttpPost("logout")] + public async Task Logout(CancellationToken cancellationToken) + { + await this.apiPlatformClient + .CareIdentityServiceClient + .LogoutAsync(cancellationToken); + + HttpContext.Session.Clear(); + await HttpContext.SignOutAsync("bff-cookie"); + + return Redirect(@""); + } + + [HttpGet("callback")] + public async Task Callback( + [FromQuery] string code, + [FromQuery] string state, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(code)) + { + return BadRequest("Authorization code is missing."); + } + + try + { + var userInfo = await this.apiPlatformClient.CareIdentityServiceClient + .GetUserInfoAsync(code, state, cancellationToken); + + string userInfoJson = JsonSerializer.Serialize(userInfo); + + User? user = await this.context.Users.FirstOrDefaultAsync( + u => u.NhsIdUserUid == userInfo.NhsIdUserUid, + cancellationToken); + + if (user is null) + { + user = new User + { + NhsIdUserUid = userInfo.NhsIdUserUid, + Name = userInfo.Name, + Sub = userInfo.Sub, + RawUserInfo = userInfoJson, + LastLoginAt = DateTime.UtcNow, + IsAuthorised = false + }; + + this.context.Users.Add(user); + } + else + { + user.LastLoginAt = DateTime.UtcNow; + user.RawUserInfo = userInfoJson; + } + + await this.context.SaveChangesAsync(cancellationToken); + + if (user.IsAuthorised is false) + { + await this.apiPlatformClient + .CareIdentityServiceClient + .LogoutAsync(cancellationToken); + + HttpContext.Session.Clear(); + await HttpContext.SignOutAsync("bff-cookie"); + + return Redirect("/unauthorised"); + } + + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, userInfo.Name), + new Claim(ClaimTypes.Name, userInfo.Name), + new Claim(ClaimTypes.Upn, userInfo.NhsIdUserUid), + }; + + var identity = new ClaimsIdentity(claims, "OAuth"); + var principal = new ClaimsPrincipal(identity); + + await HttpContext.SignInAsync("bff-cookie", principal); + + return Redirect("/"); + } + catch (CareIdentityServiceClientValidationException ex) + { + this.logger.LogWarning(ex, "OAuth callback validation failed."); + return BadRequest(ex.InnerException?.Message ?? ex.Message); + } + catch (CareIdentityServiceClientDependencyException ex) + { + this.logger.LogError(ex, "OAuth callback dependency failure."); + return StatusCode(StatusCodes.Status503ServiceUnavailable, + ex.InnerException?.Message ?? "Authentication service unavailable."); + } + catch (CareIdentityServiceClientServiceException ex) + { + this.logger.LogError(ex, "OAuth callback service failure."); + return StatusCode(StatusCodes.Status500InternalServerError, + ex.InnerException?.Message ?? "Authentication failed."); + } + } +} diff --git a/ReactApp1.Server/Controllers/PatientController.cs b/ReactApp1.Server/Controllers/PatientController.cs new file mode 100644 index 0000000..56f7e1c --- /dev/null +++ b/ReactApp1.Server/Controllers/PatientController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; + +namespace ReactApp1.Server.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class PatientController : ControllerBase +{ + private readonly IApiPlatformClient apiPlatformClient; + private readonly ILogger logger; + + public PatientController( + IApiPlatformClient apiPlatformClient, + ILogger logger) + { + this.apiPlatformClient = apiPlatformClient; + this.logger = logger; + } + + [HttpGet] + public async Task GetPatient(CancellationToken cancellationToken) + { + try + { + string body = await this.apiPlatformClient + .PersonalDemographicsServiceClient + .SearchPatientsAsync( + family: "Smith", + given: null, + gender: "female", + birthdate: new DateOnly(2010, 10, 22), + cancellationToken: cancellationToken); + + return Content(body, "application/fhir+json"); + } + catch (Exception exception) + { + this.logger.LogError(exception, "Error while searching for patients."); + + // If token is missing/expired you will typically see an unauthorized exception + // bubble up from the SDK. Return 401 as the most useful default. + return Unauthorized(); + } + } +} diff --git a/ReactApp1.Server/Data/ApplicationDbContext.cs b/ReactApp1.Server/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..6bda08f --- /dev/null +++ b/ReactApp1.Server/Data/ApplicationDbContext.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using ReactApp1.Server.Models; + +namespace ReactApp1.Server.Data; + +public class ApplicationDbContext : DbContext +{ + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + + // Add your DbSets here + public DbSet Users { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure your entities here + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.NhsIdUserUid).HasMaxLength(50).IsRequired(); + entity.Property(e => e.Name).HasMaxLength(200).IsRequired(); + entity.HasIndex(e => e.NhsIdUserUid).IsUnique(); + }); + } +} \ No newline at end of file diff --git a/ReactApp1.Server/Migrations/20260205171628_InitialCreate.Designer.cs b/ReactApp1.Server/Migrations/20260205171628_InitialCreate.Designer.cs new file mode 100644 index 0000000..aaba926 --- /dev/null +++ b/ReactApp1.Server/Migrations/20260205171628_InitialCreate.Designer.cs @@ -0,0 +1,69 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ReactApp1.Server.Data; + +#nullable disable + +namespace ReactApp1.Server.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260205171628_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ReactApp1.Server.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("NhsIdUserUid") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RawUserInfo") + .HasColumnType("nvarchar(max)"); + + b.Property("Sub") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("NhsIdUserUid") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ReactApp1.Server/Migrations/20260205171628_InitialCreate.cs b/ReactApp1.Server/Migrations/20260205171628_InitialCreate.cs new file mode 100644 index 0000000..1713021 --- /dev/null +++ b/ReactApp1.Server/Migrations/20260205171628_InitialCreate.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ReactApp1.Server.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + NhsIdUserUid = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Sub = table.Column(type: "nvarchar(max)", nullable: false), + RawUserInfo = table.Column(type: "nvarchar(max)", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + LastLoginAt = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_NhsIdUserUid", + table: "Users", + column: "NhsIdUserUid", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/ReactApp1.Server/Migrations/20260205171747_secondCreate.Designer.cs b/ReactApp1.Server/Migrations/20260205171747_secondCreate.Designer.cs new file mode 100644 index 0000000..722a5f1 --- /dev/null +++ b/ReactApp1.Server/Migrations/20260205171747_secondCreate.Designer.cs @@ -0,0 +1,72 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ReactApp1.Server.Data; + +#nullable disable + +namespace ReactApp1.Server.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260205171747_secondCreate")] + partial class secondCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ReactApp1.Server.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsAuthorised") + .HasColumnType("bit"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("NhsIdUserUid") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RawUserInfo") + .HasColumnType("nvarchar(max)"); + + b.Property("Sub") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("NhsIdUserUid") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ReactApp1.Server/Migrations/20260205171747_secondCreate.cs b/ReactApp1.Server/Migrations/20260205171747_secondCreate.cs new file mode 100644 index 0000000..8a3f53e --- /dev/null +++ b/ReactApp1.Server/Migrations/20260205171747_secondCreate.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ReactApp1.Server.Migrations +{ + /// + public partial class secondCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsAuthorised", + table: "Users", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsAuthorised", + table: "Users"); + } + } +} diff --git a/ReactApp1.Server/Migrations/ApplicationDbContextModelSnapshot.cs b/ReactApp1.Server/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..bf79bb6 --- /dev/null +++ b/ReactApp1.Server/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,69 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ReactApp1.Server.Data; + +#nullable disable + +namespace ReactApp1.Server.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ReactApp1.Server.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsAuthorised") + .HasColumnType("bit"); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("NhsIdUserUid") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RawUserInfo") + .HasColumnType("nvarchar(max)"); + + b.Property("Sub") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("NhsIdUserUid") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ReactApp1.Server/Models/NhsUserInfo.cs b/ReactApp1.Server/Models/NhsUserInfo.cs new file mode 100644 index 0000000..affeb81 --- /dev/null +++ b/ReactApp1.Server/Models/NhsUserInfo.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace ReactApp1.Server.Models; + +public class NhsUserInfo +{ + [JsonPropertyName("nhsid_useruid")] + public string NhsIdUserUid { get; set; } = default!; + + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + + [JsonPropertyName("nhsid_nrbac_roles")] + public List NhsIdNrbacRoles { get; set; } = new(); + + [JsonPropertyName("sub")] + public string Sub { get; set; } = default!; +} + +public class NhsNrbacRole +{ + [JsonPropertyName("person_orgid")] + public string PersonOrgId { get; set; } = default!; + + [JsonPropertyName("person_roleid")] + public string PersonRoleId { get; set; } = default!; + + [JsonPropertyName("org_code")] + public string OrgCode { get; set; } = default!; + + [JsonPropertyName("role_name")] + public string RoleName { get; set; } = default!; + + [JsonPropertyName("role_code")] + public string RoleCode { get; set; } = default!; + + [JsonPropertyName("activities")] + public List Activities { get; set; } = new(); + + [JsonPropertyName("activity_codes")] + public List ActivityCodes { get; set; } = new(); +} \ No newline at end of file diff --git a/ReactApp1.Server/Models/User.cs b/ReactApp1.Server/Models/User.cs new file mode 100644 index 0000000..ef6f9bc --- /dev/null +++ b/ReactApp1.Server/Models/User.cs @@ -0,0 +1,13 @@ +namespace ReactApp1.Server.Models; + +public class User +{ + public int Id { get; set; } + public string NhsIdUserUid { get; set; } = default!; + public string Name { get; set; } = default!; + public string Sub { get; set; } = default!; + public string? RawUserInfo { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? LastLoginAt { get; set; } + public bool IsAuthorised { get; set; } = false; +} \ No newline at end of file diff --git a/ReactApp1.Server/Program.cs b/ReactApp1.Server/Program.cs new file mode 100644 index 0000000..72877f9 --- /dev/null +++ b/ReactApp1.Server/Program.cs @@ -0,0 +1,100 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; +using NHS.Digital.ApiPlatform.Sdk; +using NHS.Digital.ApiPlatform.Sdk.AspNetCore; +using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; +using ReactApp1.Server.Data; + +var builder = WebApplication.CreateBuilder(args); + +// Add DbContext +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("iDecide"))); + +builder.Services.AddHttpClient(); + +builder.Services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = builder.Configuration.GetConnectionString("SessionCache"); + options.SchemaName = "dbo"; + options.TableName = "SessionCache"; +}); + +builder.Services.AddSession(options => +{ + options.IdleTimeout = TimeSpan.FromMinutes(30); + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Cookie.SameSite = SameSiteMode.Lax; // ✅ CRITICAL FIX: Changed from Strict + options.Cookie.Name = ".IDecide.Session"; // ✅ Explicit naming +}); + +// SECURITY FIX: Store keys securely (NOT in c:\temp) +var keysPath = builder.Environment.IsProduction() + ? Path.Combine(builder.Environment.ContentRootPath, "DataProtectionKeys") + : Path.Combine(Path.GetTempPath(), "IDecide-DataProtection-Dev"); + +Directory.CreateDirectory(keysPath); + +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(keysPath)) + .SetApplicationName("IDecide") + .SetDefaultKeyLifetime(TimeSpan.FromDays(90)); + +builder.Services.AddAuthentication("bff-cookie") + .AddCookie("bff-cookie", options => + { + options.LoginPath = "/Login"; + options.LogoutPath = "/Logout"; + options.ExpireTimeSpan = TimeSpan.FromDays(7); + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Cookie.SameSite = SameSiteMode.Lax; // ✅ Also fix for auth cookie + options.Cookie.Name = "bff-cookie"; + }); + +// NHS Digital API Platform SDK (Core + AspNetCore/session storage) +ApiPlatformConfigurations apiPlatformConfigurations = new() +{ + CareIdentity = new CareIdentityConfigurations + { + ClientId = builder.Configuration["CIS:ClientId"] ?? string.Empty, + ClientSecret = builder.Configuration["CIS:ClientSecret"] ?? string.Empty, + RedirectUri = builder.Configuration["CIS:RedirectUri"] ?? string.Empty, + AuthEndpoint = builder.Configuration["CIS:AuthEndpoint"] ?? string.Empty, + TokenEndpoint = builder.Configuration["CIS:TokenEndpoint"] ?? string.Empty, + UserInfoEndpoint = builder.Configuration["CIS:UserInfoEndpoint"] ?? string.Empty, + AcrValues = builder.Configuration["CIS:AALLevel"] + }, + PersonalDemographicsService = new PersonalDemographicsServiceConfigurations + { + BaseUrl = builder.Configuration["PDS:BaseUrl"] + ?? "https://int.api.service.nhs.uk/personal-demographics/FHIR/R4" + } +}; + +builder.Services.AddApiPlatformSdkCore(apiPlatformConfigurations); +builder.Services.AddApiPlatformSdkAspNetCore(); + +builder.Services.AddControllers(); +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +app.UseDefaultFiles(); +app.MapStaticAssets(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseSession(); +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +app.MapFallbackToFile("/index.html"); +app.Run(); diff --git a/ReactApp1.Server/Properties/launchSettings.json b/ReactApp1.Server/Properties/launchSettings.json new file mode 100644 index 0000000..cf5f071 --- /dev/null +++ b/ReactApp1.Server/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5257", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7202;https://localhost:5257", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + } + } +} + diff --git a/ReactApp1.Server/ReactApp1.Server.csproj b/ReactApp1.Server/ReactApp1.Server.csproj new file mode 100644 index 0000000..1dad46d --- /dev/null +++ b/ReactApp1.Server/ReactApp1.Server.csproj @@ -0,0 +1,38 @@ + + + + net10.0 + enable + enable + ..\reactapp1.client + npm run dev + https://localhost:62388 + 6f274465-3968-4737-8105-5893965268bf + + + + + + + + 10.0.3 + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/ReactApp1.Server/ReactApp1.Server.http b/ReactApp1.Server/ReactApp1.Server.http new file mode 100644 index 0000000..5cb2283 --- /dev/null +++ b/ReactApp1.Server/ReactApp1.Server.http @@ -0,0 +1,6 @@ +@ReactApp1.Server_HostAddress = http://localhost:5257 + +GET {{ReactApp1.Server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/ReactApp1.Server/Services/ITokenService.cs b/ReactApp1.Server/Services/ITokenService.cs new file mode 100644 index 0000000..b773b68 --- /dev/null +++ b/ReactApp1.Server/Services/ITokenService.cs @@ -0,0 +1,6 @@ +namespace ReactApp1.Server.Services; + +public interface ITokenService +{ + Task GetAccessTokenAsync(HttpContext httpContext); +} \ No newline at end of file diff --git a/ReactApp1.Server/Services/SecureTokenStorage.cs b/ReactApp1.Server/Services/SecureTokenStorage.cs new file mode 100644 index 0000000..649262b --- /dev/null +++ b/ReactApp1.Server/Services/SecureTokenStorage.cs @@ -0,0 +1,213 @@ +using Microsoft.AspNetCore.DataProtection; +using System.Text; + +namespace ReactApp1.Server.Services; + +public interface ISecureTokenStorage +{ + void StoreAccessToken(HttpContext context, string token, int expiresInSeconds); + void StoreRefreshToken(HttpContext context, string token, int expiresInSeconds); + string? GetAccessToken(HttpContext context); + string? GetRefreshToken(HttpContext context); + void ClearTokens(HttpContext context); + void StoreCSRFState(HttpContext httpContext, string csrfState); + string GetCSRFState(HttpContext httpContext); + void ClearCSRFState(HttpContext httpContext); +} + +public class SecureTokenStorage : ISecureTokenStorage +{ + private readonly IDataProtector _protector; + private readonly ILogger _logger; + + private const string AccessTokenKey = "secure_access_token"; + private const string AccessTokenExpiresKey = "access_token_expires_at"; + private const string RefreshTokenKey = "secure_refresh_token"; + private const string RefreshTokenExpiresKey = "refresh_token_expires_at"; + private const string CRFSStateKey = "CRFS_State"; + + public SecureTokenStorage( + IDataProtectionProvider dataProtectionProvider, + ILogger logger) + { + // Create a dedicated protector for OAuth tokens + _protector = dataProtectionProvider.CreateProtector("OAuthTokenProtection"); + _logger = logger; + } + + public void StoreAccessToken(HttpContext context, string token, int expiresInSeconds) + { + if (string.IsNullOrEmpty(token)) + { + _logger.LogWarning("Attempted to store null or empty access token"); + return; + } + + try + { + // Encrypt token before storing + var encryptedToken = _protector.Protect(Encoding.UTF8.GetBytes(token)); + var base64Token = Convert.ToBase64String(encryptedToken); + + context.Session.SetString(AccessTokenKey, base64Token); + context.Session.SetString(AccessTokenExpiresKey, + DateTime.UtcNow.AddSeconds(expiresInSeconds).ToString("O")); // ISO 8601 format + + _logger.LogDebug("Access token securely stored (expires in {Seconds}s)", expiresInSeconds); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to encrypt and store access token"); + throw; + } + } + + public void StoreRefreshToken(HttpContext context, string token, int expiresInSeconds) + { + if (string.IsNullOrEmpty(token)) + { + _logger.LogWarning("Attempted to store null or empty refresh token"); + return; + } + + try + { + var encryptedToken = _protector.Protect(Encoding.UTF8.GetBytes(token)); + var base64Token = Convert.ToBase64String(encryptedToken); + + context.Session.SetString(RefreshTokenKey, base64Token); + context.Session.SetString(RefreshTokenExpiresKey, + DateTime.UtcNow.AddSeconds(expiresInSeconds).ToString("O")); + + _logger.LogDebug("Refresh token securely stored (expires in {Seconds}s)", expiresInSeconds); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to encrypt and store refresh token"); + throw; + } + } + + public string? GetAccessToken(HttpContext context) + { + try + { + var encryptedBase64 = context.Session.GetString(AccessTokenKey); + if (string.IsNullOrEmpty(encryptedBase64)) + { + return null; + } + + // Check expiration + var expiresAtStr = context.Session.GetString(AccessTokenExpiresKey); + if (string.IsNullOrEmpty(expiresAtStr) || + !DateTime.TryParse(expiresAtStr, out var expiresAt) || + DateTime.UtcNow >= expiresAt) + { + _logger.LogDebug("Access token expired or invalid expiration"); + return null; + } + + // Decrypt token + var encryptedBytes = Convert.FromBase64String(encryptedBase64); + var decryptedBytes = _protector.Unprotect(encryptedBytes); + return Encoding.UTF8.GetString(decryptedBytes); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to decrypt access token"); + return null; + } + } + + public string? GetRefreshToken(HttpContext context) + { + try + { + var encryptedBase64 = context.Session.GetString(RefreshTokenKey); + if (string.IsNullOrEmpty(encryptedBase64)) + { + return null; + } + + // Check expiration + var expiresAtStr = context.Session.GetString(RefreshTokenExpiresKey); + if (string.IsNullOrEmpty(expiresAtStr) || + !DateTime.TryParse(expiresAtStr, out var expiresAt) || + DateTime.UtcNow >= expiresAt) + { + _logger.LogDebug("Refresh token expired or invalid expiration"); + return null; + } + + var encryptedBytes = Convert.FromBase64String(encryptedBase64); + var decryptedBytes = _protector.Unprotect(encryptedBytes); + return Encoding.UTF8.GetString(decryptedBytes); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to decrypt refresh token"); + return null; + } + } + + public void ClearTokens(HttpContext context) + { + context.Session.Remove(AccessTokenKey); + context.Session.Remove(AccessTokenExpiresKey); + context.Session.Remove(RefreshTokenKey); + context.Session.Remove(RefreshTokenExpiresKey); + _logger.LogDebug("All tokens cleared from session"); + } + + public void StoreCSRFState(HttpContext context, string csrfState) + { + if (string.IsNullOrEmpty(csrfState)) + { + _logger.LogWarning("Attempted to store null or empty state."); + return; + } + + try + { + var encryptedToken = _protector.Protect(Encoding.UTF8.GetBytes(csrfState)); + var base64Token = Convert.ToBase64String(encryptedToken); + + context.Session.SetString(CRFSStateKey, base64Token); + + _logger.LogDebug($"State securely stored: {csrfState}"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to encrypt and store state."); + throw; + } + } + + public string GetCSRFState(HttpContext context) + { + try + { + var encryptedBase64 = context.Session.GetString(CRFSStateKey); + if (string.IsNullOrEmpty(encryptedBase64)) + { + return null; + } + + var encryptedBytes = Convert.FromBase64String(encryptedBase64); + var decryptedBytes = _protector.Unprotect(encryptedBytes); + return Encoding.UTF8.GetString(decryptedBytes); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to decrypt refresh token"); + return null; + } + } + + public void ClearCSRFState(HttpContext context) + { + context.Session.Remove(CRFSStateKey); + _logger.LogDebug("CRFS state cleared from session"); + } +} \ No newline at end of file diff --git a/ReactApp1.Server/Services/TokenService.cs b/ReactApp1.Server/Services/TokenService.cs new file mode 100644 index 0000000..26d2910 --- /dev/null +++ b/ReactApp1.Server/Services/TokenService.cs @@ -0,0 +1,129 @@ +using Microsoft.AspNetCore.Authentication; +using System.Text.Json; + +namespace ReactApp1.Server.Services; + +public class TokenService : ITokenService +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly ISecureTokenStorage _secureTokenStorage; + + private readonly string _tokenEndpoint; + private readonly string _clientId; + private readonly string _clientSecret; + + public TokenService( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + ILogger logger, + ISecureTokenStorage secureTokenStorage) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + _logger = logger; + _secureTokenStorage = secureTokenStorage; + + _tokenEndpoint = _configuration["CIS:TokenEndpoint"] ?? ""; + _clientId = _configuration["CIS:ClientId"] ?? ""; + _clientSecret = _configuration["CIS:ClientSecret"] ?? ""; + } + + public async Task GetAccessTokenAsync(HttpContext httpContext) + { + var accessToken = _secureTokenStorage.GetAccessToken(httpContext); + + if (accessToken != null) + { + _logger.LogDebug("Valid access token retrieved from secure storage"); + return accessToken; + } + + _logger.LogInformation("Access token expired or missing, attempting refresh"); + + var refreshed = await RefreshAccessTokenAsync(httpContext); + if (!refreshed) + { + _logger.LogWarning("Token refresh failed"); + return null; + } + + return _secureTokenStorage.GetAccessToken(httpContext); + } + + private async Task RefreshAccessTokenAsync(HttpContext httpContext) + { + var refreshToken = _secureTokenStorage.GetRefreshToken(httpContext); + + if (string.IsNullOrEmpty(refreshToken)) + { + _logger.LogWarning("Refresh token not found in secure storage"); + await SignOutAsync(httpContext); + return false; + } + + try + { + var client = _httpClientFactory.CreateClient(); + + var response = await client.PostAsync( + _tokenEndpoint, + new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "refresh_token", + ["refresh_token"] = refreshToken, + ["client_id"] = _clientId, + ["client_secret"] = _clientSecret + }) + ); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogError("CIS2 token refresh failed: {StatusCode} - {Error}", + response.StatusCode, errorContent); + await SignOutAsync(httpContext); + return false; + } + + var json = await response.Content.ReadAsStringAsync(); + var parsedToken = JsonSerializer.Deserialize(json); + + if (parsedToken == null || string.IsNullOrEmpty(parsedToken.AccessToken)) + { + _logger.LogError("Failed to deserialize refresh token response"); + await SignOutAsync(httpContext); + return false; + } + + // ✅ Store refreshed tokens encrypted + if (int.TryParse(parsedToken.ExpiresIn, out int accessExpires)) + { + _secureTokenStorage.StoreAccessToken(httpContext, parsedToken.AccessToken, accessExpires); + } + + if (!string.IsNullOrEmpty(parsedToken.RefreshToken) && + int.TryParse(parsedToken.RefreshTokenExpiresIn, out int refreshExpires)) + { + _secureTokenStorage.StoreRefreshToken(httpContext, parsedToken.RefreshToken, refreshExpires); + } + + _logger.LogInformation("Successfully refreshed access token"); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing access token"); + await SignOutAsync(httpContext); + return false; + } + } + + private async Task SignOutAsync(HttpContext httpContext) + { + _secureTokenStorage.ClearTokens(httpContext); + httpContext.Session.Clear(); + await httpContext.SignOutAsync("bff-cookie"); + } +} \ No newline at end of file diff --git a/ReactApp1.Server/TokenResult.cs b/ReactApp1.Server/TokenResult.cs new file mode 100644 index 0000000..3938e20 --- /dev/null +++ b/ReactApp1.Server/TokenResult.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace ReactApp1.Server +{ + public sealed class TokenResult + { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = default!; + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } = default!; + + [JsonPropertyName("expires_in")] + public string ExpiresIn { get; set; } + + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } = default!; + + [JsonPropertyName("refresh_token_expires_in")] + public string RefreshTokenExpiresIn { get; set; } + } +} diff --git a/ReactApp1.Server/appsettings.json b/ReactApp1.Server/appsettings.json new file mode 100644 index 0000000..49a3340 --- /dev/null +++ b/ReactApp1.Server/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.Authentication": "Trace", + "Microsoft.IdentityModel": "Trace" + } + }, + "AllowedHosts": "*" +} diff --git a/Resources/Images/NhsDigitalBanner.png b/Resources/Images/NhsDigitalBanner.png new file mode 100644 index 0000000000000000000000000000000000000000..6eb8dbd0d45511364a9e8902ee3e297d469a895e GIT binary patch literal 50568 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 T?m>ThldhJ*qcV*b|NVaecyj1T literal 0 HcmV?d00001 diff --git a/Resources/Images/NhsDigitalIcon.png b/Resources/Images/NhsDigitalIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..8e38ba65f0db85091288c52a9ba1e7893c916a53 GIT binary patch literal 57251 zcmYhib9iJ=)Ggd`CbsQ~ZQGgHwl$g9p4i63c4lJRwryK|`*+{>e$V&Ec}_por%zQ^ z?Y-ApdsjvNP?SQ1!-E3=0EjZu;wk_DIOrod02Uf_d+=Cz1KqwkNo%_T0Lp0pU1030 zrlz1q+@BI!KUEzqe!3gGm;>D1-5ISNY+X%_oy-{>T`aS%`0)S$5`c`jh?+VN^!R z5IM${3@AgRmUIil%8E)>aumRaADg;>9s!4|D+~&5M9&~$ozOrZ6JTO;a^ZnM;8+OA zLe_^l5(or;;p*`R?|?P{KdKP2nk|rs!eY?bz=wPRd?0)X^#H$pYzK5-q)lQX_4vbo zO!vh7bB$)uY5KoU^7U*3fao`lT>f>ZfS&D@_*w2 zb+1je-5|Td%IlSRNP zLyAYRpqT;!>0L2A6;sV|AkzPS@3rp%$AMK&XCl8h-?8gBAok0>jk_-GAf}JR0mrhg z?ZGbOb)ACE2%YE|7aPu^j^$Ef^?U3XcCUSUxf&!Yl-L3N;{N&C@?-bs)cZY@P!3ai zN$qXp>DklYapUD#It1^~8bdL)byQJ`5b0=CVsJqZQP>a^6jTbh z#4G6F*+GZ8;1A`}rOa((3M)@4D z0cXei&m4v7zUSxF>#0v~H}EsUAJ|UX&6j~*2T5O#%WcCn)chl-W&^f0bfd2}mayA1 zm(4H1l=9AO`u5Gb_a~9ZcHaH7kkU(*km5@z@>z5Yc0&JWQ2!@Enq{=yaFH4{pg$7= z#k?QPs#Y^}@v(Fn7a%Q>JIhE!a+@OtrA@|J2UW%0Ae_3{OL01Nbe071aZ>$&GnCQ?rh84tu8 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

&nv)<*M=C-O*Nf9`qtd^K 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 + + + + + + reactapp1.client + + +

+ + + diff --git a/reactapp1.client/package-lock.json b/reactapp1.client/package-lock.json new file mode 100644 index 0000000..b3b168b --- /dev/null +++ b/reactapp1.client/package-lock.json @@ -0,0 +1,2872 @@ +{ + "name": "reactapp1.client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reactapp1.client", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.10.tgz", + "integrity": "sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/reactapp1.client/package.json b/reactapp1.client/package.json new file mode 100644 index 0000000..7c2e606 --- /dev/null +++ b/reactapp1.client/package.json @@ -0,0 +1,27 @@ +{ + "name": "reactapp1.client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.4" + } +} diff --git a/reactapp1.client/public/vite.svg b/reactapp1.client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/reactapp1.client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/reactapp1.client/reactapp1.client.esproj b/reactapp1.client/reactapp1.client.esproj new file mode 100644 index 0000000..6e48911 --- /dev/null +++ b/reactapp1.client/reactapp1.client.esproj @@ -0,0 +1,11 @@ + + + npm run dev + src\ + Vitest + + false + + $(MSBuildProjectDirectory)\dist + + \ No newline at end of file diff --git a/reactapp1.client/src/App.css b/reactapp1.client/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/reactapp1.client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/reactapp1.client/src/App.jsx b/reactapp1.client/src/App.jsx new file mode 100644 index 0000000..e1d2012 --- /dev/null +++ b/reactapp1.client/src/App.jsx @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react' +import Viewer from './viewer' + +function App() { + + const [tokens, setTokens] = useState(); + useEffect(() => { + var r = fetch('/auth/session').then(d => d.json()).then(r => setTokens(r)); + },[]); + + + const logout = () => { + fetch('/auth/logout', { method: "POST" }); + } + + return ( + <> + login +
+ Patient + +
{JSON.stringify(tokens, null, 2)}
+ + + ) +} + +export default App diff --git a/reactapp1.client/src/PatientView.tsx b/reactapp1.client/src/PatientView.tsx new file mode 100644 index 0000000..0d79f55 --- /dev/null +++ b/reactapp1.client/src/PatientView.tsx @@ -0,0 +1,10 @@ +import { PatientType } from "./types/fhir" + +const PatientView = (patient: PatientType) => { + + + +} + +export default PatientView + diff --git a/reactapp1.client/src/assets/react.svg b/reactapp1.client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/reactapp1.client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/reactapp1.client/src/index.css b/reactapp1.client/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/reactapp1.client/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/reactapp1.client/src/main.jsx b/reactapp1.client/src/main.jsx new file mode 100644 index 0000000..3d9da8a --- /dev/null +++ b/reactapp1.client/src/main.jsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/reactapp1.client/src/patient.jsx b/reactapp1.client/src/patient.jsx new file mode 100644 index 0000000..bbc5bb8 --- /dev/null +++ b/reactapp1.client/src/patient.jsx @@ -0,0 +1,3102 @@ +export const patient = { + "entry": [ + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "1711" + }, + { + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/CareConnect-NHSNumberVerificationStatus-1", + "code": "01", + "display": "Number present and verified" + } + ] + }, + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-NHSNumberVerificationStatus-1" + } + ], + "system": "https://fhir.hl7.org.uk/Id/nhs-number", + "value": "5558526785" + } + ], + "extension": [ + { + "extension": [ + { + "valuePeriod": { + "start": "2013-05-29T00:00:00+00:00" + }, + "url": "registrationPeriod" + }, + { + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/ValueSet/CareConnect-RegistrationType-1", + "display": "Regular/GMS" + } + ] + }, + "url": "registrationType" + } + ], + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-RegistrationDetails-1" + } + ], + "address": [ + { + "city": "Truro", + "use": "home", + "line": [ + "Road" + ] + } + ], + "gender": "male", + "birthDate": "1970-01-01", + "managingOrganization": { + "reference": "Organization/5ff06392-92cb-4e43-a4cf-d7d683d09197" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Patient-1" + ] + }, + "generalPractitioner": [ + { + "reference": "Practitioner/75bc169c-df60-41d5-9782-5e785529eb40" + } + ], + "name": [ + { + "given": [ + "John" + ], + "use": "official", + "prefix": [ + "Mr" + ], + "family": "Smith" + } + ], + "telecom": [ + { + "system": "phone", + "use": "mobile", + "value": "02083456788" + } + ], + "id": "bf3904da-c11f-4004-a774-f6049cb8308e", + "communication": [ + { + "language": { + "coding": [ + { + "code": "13lS.", + "display": "Main spoken language Albanian" + } + ] + } + } + ], + "resourceType": "Patient" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73520" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "TJ961", + "display": "AR - Lysergide - LSD" + } + ], + "text": "AR - Lysergide - LSD" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "3f1962be-d1f6-40fa-9f4e-23689cc928bc", + "clinicalStatus": "active", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2014-01-30T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73523" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Not significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "1B1G.", + "display": "Headache" + } + ], + "text": "Headache" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "45cd5f93-2305-47a9-bb72-e33348faf9ba", + "clinicalStatus": "resolved", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2013-12-20T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73527" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "13M4.", + "display": "Death Of Pet" + } + ], + "text": "Death Of Pet" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "d4124ddb-6c9d-436c-9d12-5694076694e5", + "clinicalStatus": "resolved", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2013-12-05T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73531" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "EMISNOFH5", + "display": "No FH: Diabetes" + } + ], + "text": "No FH: Diabetes" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "ee51adf7-77ee-4af7-9fe6-d66cd9e6916d", + "clinicalStatus": "resolved", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2013-12-05T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73533" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SN52.", + "display": "Drug Hypersensitivity NOS" + } + ], + "text": "Drug Hypersensitivity NOS" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "4ead2ff8-1ca5-4109-9d9b-906506838214", + "clinicalStatus": "resolved", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2013-12-04T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73537" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "TJ53.", + "display": "AR - Salicylates" + } + ], + "text": "AR - Salicylates" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "2346ffa1-1aa5-4057-98ae-8945abd6fe55", + "clinicalStatus": "active", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2013-12-04T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73539" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Not significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "182..", + "display": "Chest Pain" + } + ], + "text": "Chest Pain" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "c8dcfb0e-5290-4e1a-a2cf-091aa681e1f8", + "clinicalStatus": "active", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2014-02-18T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73545" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SN580", + "display": "Egg Allergy" + } + ], + "text": "Egg Allergy" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "5b331764-353a-4583-a0b1-5cf6e82316c0", + "clinicalStatus": "active", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2014-01-30T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73548" + } + ], + "extension": [ + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://endeavourhealth.org/fhir/ValueSet/primarycare-problem-significance", + "display": "Not significant (qualifier value)" + } + ] + }, + "url": "http://endeavourhealth.org/fhir/StructureDefinition/primarycare-problem-significance-extension" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "TG808", + "display": "Accid.-scald-chocolate" + } + ], + "text": "Accid.-scald-chocolate" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ProblemHeader-Condition-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "0472a4f2-7470-40da-a5c0-42400c8a98a7", + "clinicalStatus": "resolved", + "category": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-category", + "code": "problem-list-item", + "display": "Problem list Item" + } + ] + } + ], + "onsetDateTime": "2013-12-05T00:00:00+00:00", + "resourceType": "Condition" + } + }, + { + "resource": { + "mode": "snapshot", + "date": "2026-01-23T11:00:18+00:00", + "entry": [ + { + "item": { + "reference": "Condition/3f1962be-d1f6-40fa-9f4e-23689cc928bc" + } + }, + { + "item": { + "reference": "Condition/45cd5f93-2305-47a9-bb72-e33348faf9ba" + } + }, + { + "item": { + "reference": "Condition/d4124ddb-6c9d-436c-9d12-5694076694e5" + } + }, + { + "item": { + "reference": "Condition/ee51adf7-77ee-4af7-9fe6-d66cd9e6916d" + } + }, + { + "item": { + "reference": "Condition/4ead2ff8-1ca5-4109-9d9b-906506838214" + } + }, + { + "item": { + "reference": "Condition/2346ffa1-1aa5-4057-98ae-8945abd6fe55" + } + }, + { + "item": { + "reference": "Condition/c8dcfb0e-5290-4e1a-a2cf-091aa681e1f8" + } + }, + { + "item": { + "reference": "Condition/5b331764-353a-4583-a0b1-5cf6e82316c0" + } + }, + { + "item": { + "reference": "Condition/0472a4f2-7470-40da-a5c0-42400c8a98a7" + } + } + ], + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-List-1" + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "title": "Problems", + "resourceType": "List", + "status": "current" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73962" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "TJ961", + "display": "AR - Lysergide - LSD" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/89adffa3-0342-407e-9f65-4f2ff39cfebf" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "675225a6-aa26-415a-948e-def17bc45e53", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2014-01-29T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73963" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SN589", + "display": "Allergy To Strawberries" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/89adffa3-0342-407e-9f65-4f2ff39cfebf" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "6c0d05c8-f499-479c-a33f-9c4d9a650671", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2014-01-30T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73964" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SN580", + "display": "Egg Allergy" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/89adffa3-0342-407e-9f65-4f2ff39cfebf" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "5e1bfa2e-c2d7-4ff2-b496-5a76121da661", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2014-01-30T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73965" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "TJ961", + "display": "AR - Lysergide - LSD" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/89adffa3-0342-407e-9f65-4f2ff39cfebf" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "e57a0b61-741a-4734-b372-dee53b0169e4", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2014-01-30T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73966" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SN583", + "display": "Nut Allergy" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/89adffa3-0342-407e-9f65-4f2ff39cfebf" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "b6e88275-0a9b-4a92-90bb-54e6e5bd5938", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2014-01-30T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73967" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SN52.", + "display": "Drug Hypersensitivity NOS" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/66d19ac6-ce1b-4623-b860-372634dca807" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "e8565f72-9701-478f-abe3-d88471b8a51a", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2013-12-05T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73968" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "TJ961", + "display": "AR - Lysergide - LSD" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/89adffa3-0342-407e-9f65-4f2ff39cfebf" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "c3e0b81c-ecf4-4865-ac02-24f02ab839d2", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2014-01-29T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73969" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "SN52.", + "display": "Drug Hypersensitivity NOS" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/0256ac41-0938-4608-a9ea-1826c6593194" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "78d355a2-cfdd-4d98-a04d-8b33d5a791e2", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2013-12-04T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73970" + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "TJ53.", + "display": "AR - Salicylates" + } + ] + }, + "asserter": { + "reference": "PractitionerRole/0256ac41-0938-4608-a9ea-1826c6593194" + }, + "verificationStatus": "confirmed", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-AllergyIntolerance-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "id": "95546cd3-d2f6-4313-986d-f538947ffb82", + "clinicalStatus": "active", + "type": "allergy", + "onsetDateTime": "2013-12-04T00:00:00+00:00", + "resourceType": "AllergyIntolerance" + } + }, + { + "resource": { + "mode": "snapshot", + "date": "2026-01-23T11:00:18+00:00", + "entry": [ + { + "item": { + "reference": "AllergyIntolerance/675225a6-aa26-415a-948e-def17bc45e53" + } + }, + { + "item": { + "reference": "AllergyIntolerance/6c0d05c8-f499-479c-a33f-9c4d9a650671" + } + }, + { + "item": { + "reference": "AllergyIntolerance/5e1bfa2e-c2d7-4ff2-b496-5a76121da661" + } + }, + { + "item": { + "reference": "AllergyIntolerance/e57a0b61-741a-4734-b372-dee53b0169e4" + } + }, + { + "item": { + "reference": "AllergyIntolerance/b6e88275-0a9b-4a92-90bb-54e6e5bd5938" + } + }, + { + "item": { + "reference": "AllergyIntolerance/e8565f72-9701-478f-abe3-d88471b8a51a" + } + }, + { + "item": { + "reference": "AllergyIntolerance/c3e0b81c-ecf4-4865-ac02-24f02ab839d2" + } + }, + { + "item": { + "reference": "AllergyIntolerance/78d355a2-cfdd-4d98-a04d-8b33d5a791e2" + } + }, + { + "item": { + "reference": "AllergyIntolerance/95546cd3-d2f6-4313-986d-f538947ffb82" + } + } + ], + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "886921000000105", + "display": "Allergies and adverse reaction" + } + ] + }, + "orderedBy": { + "coding": [ + { + "system": "http://hl7.org/fhir/list-order", + "code": " event-date" + } + ] + }, + "meta": { + "profile": [ + "https://fhir.nhs.uk/STU3/StructureDefinition/CareConnect-GPC-List-1" + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "title": "Active Allergies", + "resourceType": "List", + "status": "current" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73914" + } + ], + "medicationReference": { + "reference": "Medication/8560eb71-dc3c-4d8a-b001-33a1e0eb74bb" + }, + "dosage": [ + { + "text": "One To Be Taken At Night" + } + ], + "extension": [ + { + "valueDateTime": "2013-11-28T00:00:00+00:00", + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-MedicationStatementLastIssueDate-1" + } + ], + "informationSource": { + "reference": "PractitionerRole/c0b86203-a8de-457e-b893-964d4bd558a6" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-MedicationStatement-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "taken": "unk", + "id": "887ead30-86a8-47c7-9735-a072b10a9549", + "dateAsserted": "2013-11-28T00:00:00+00:00", + "resourceType": "MedicationStatement", + "status": "active" + } + }, + { + "resource": { + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "321127001", + "display": "nitrazepam 5 milligram/1 each conventional release oral tablet " + } + ] + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Medication-1" + ] + }, + "id": "8560eb71-dc3c-4d8a-b001-33a1e0eb74bb", + "resourceType": "Medication" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73916" + } + ], + "medicationReference": { + "reference": "Medication/698351c4-99b5-441a-9dc4-180f4ffa3541" + }, + "dosage": [ + { + "text": "One To Be Taken Every 4-6 Hours Up To Four Times A Day" + } + ], + "extension": [ + { + "valueDateTime": "2014-02-18T00:00:00+00:00", + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-MedicationStatementLastIssueDate-1" + } + ], + "informationSource": { + "reference": "PractitionerRole/a23bb040-3d51-47dd-a9cc-a853db62e860" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-MedicationStatement-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "taken": "unk", + "id": "b15c4ad2-c28f-4ea9-984a-03b116d09ac7", + "dateAsserted": "2014-02-18T00:00:00+00:00", + "resourceType": "MedicationStatement", + "status": "active" + } + }, + { + "resource": { + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "322280009", + "display": "paracetamol 500 milligram/1 each conventional release oral capsule " + } + ] + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Medication-1" + ] + }, + "id": "698351c4-99b5-441a-9dc4-180f4ffa3541", + "resourceType": "Medication" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73903" + } + ], + "medicationReference": { + "reference": "Medication/797b6c46-6404-48ed-b85f-e144b1913d44" + }, + "dosage": [ + { + "text": "1 Gram To Be Inserted Each Day" + } + ], + "extension": [ + { + "valueDateTime": "2014-01-27T00:00:00+00:00", + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-MedicationStatementLastIssueDate-1" + } + ], + "informationSource": { + "reference": "PractitionerRole/308d34a6-23b4-48c2-bbf0-38ba493c91e9" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-MedicationStatement-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "taken": "unk", + "id": "55d06ed3-6643-44c5-ab15-6015d0683dfd", + "dateAsserted": "2014-01-27T00:00:00+00:00", + "resourceType": "MedicationStatement", + "status": "active" + } + }, + { + "resource": { + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "329807003", + "display": "naproxen 500 milligram/1 each conventional release oral tablet " + } + ] + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Medication-1" + ] + }, + "id": "797b6c46-6404-48ed-b85f-e144b1913d44", + "resourceType": "Medication" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73921" + } + ], + "medicationReference": { + "reference": "Medication/eff439b8-d40e-452e-a1e8-6df9e8b8088a" + }, + "dosage": [ + { + "text": "1 to be taken 3 times a day" + } + ], + "extension": [ + { + "valueDateTime": "2014-07-25T00:00:00+00:00", + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-MedicationStatementLastIssueDate-1" + } + ], + "informationSource": { + "reference": "PractitionerRole/272d8751-f659-4263-b12e-f6c996a6e19b" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-MedicationStatement-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "taken": "unk", + "id": "cc6bc75d-3c1b-48cb-b1be-ffba0d93d3a5", + "dateAsserted": "2014-07-25T00:00:00+00:00", + "resourceType": "MedicationStatement", + "status": "active" + } + }, + { + "resource": { + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "323734001", + "display": "Amoxicillin 125mg/1.25ml oral suspension paediatric" + } + ] + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Medication-1" + ] + }, + "id": "eff439b8-d40e-452e-a1e8-6df9e8b8088a", + "resourceType": "Medication" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73915" + } + ], + "medicationReference": { + "reference": "Medication/66ac8ddd-26a9-4e79-8f7d-ff362110ea45" + }, + "dosage": [ + { + "text": "Apply To Wet Skin And Rinse" + } + ], + "extension": [ + { + "valueDateTime": "2013-12-19T00:00:00+00:00", + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-MedicationStatementLastIssueDate-1" + } + ], + "informationSource": { + "reference": "PractitionerRole/ce01a137-45e1-4142-84e2-0d16263feacc" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-MedicationStatement-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "taken": "unk", + "id": "1d845585-400f-4a10-b8a3-b31dd4dd4621", + "dateAsserted": "2013-12-02T00:00:00+00:00", + "resourceType": "MedicationStatement", + "status": "active" + } + }, + { + "resource": { + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "3486211000001108", + "display": "Liquid paraffin light 70% gel " + } + ] + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Medication-1" + ] + }, + "id": "66ac8ddd-26a9-4e79-8f7d-ff362110ea45", + "resourceType": "Medication" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73922" + } + ], + "medicationReference": { + "reference": "Medication/347b79ea-b691-465d-b548-b6f6fc38c375" + }, + "dosage": [ + { + "text": "One To Be Taken Every 4-6 Hours Up To Four Times A Day" + } + ], + "extension": [ + { + "valueDateTime": "2014-07-28T00:00:00+00:00", + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-MedicationStatementLastIssueDate-1" + } + ], + "informationSource": { + "reference": "PractitionerRole/272d8751-f659-4263-b12e-f6c996a6e19b" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-MedicationStatement-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "taken": "unk", + "id": "8bd34353-6d29-42fa-93e4-d64d34b13f2c", + "dateAsserted": "2014-07-25T00:00:00+00:00", + "resourceType": "MedicationStatement", + "status": "active" + } + }, + { + "resource": { + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "322280009", + "display": "paracetamol 500 milligram/1 each conventional release oral capsule " + } + ] + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Medication-1" + ] + }, + "id": "347b79ea-b691-465d-b548-b6f6fc38c375", + "resourceType": "Medication" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73900" + } + ], + "medicationReference": { + "reference": "Medication/9294f77d-da2e-4051-bf42-b1baaf714139" + }, + "dosage": [ + { + "text": "One To Be Taken Every 4-6 Hours Up To Four Times A Day" + } + ], + "extension": [ + { + "valueDateTime": "2014-02-21T00:00:00+00:00", + "url": "https://fhir.hl7.org.uk/STU3/StructureDefinition/Extension-CareConnect-MedicationStatementLastIssueDate-1" + } + ], + "informationSource": { + "reference": "PractitionerRole/717b7638-2b37-49eb-83de-4e98a4dfcb73" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-MedicationStatement-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "taken": "unk", + "id": "643a0883-efbc-4c49-9695-31a2442e88f3", + "dateAsserted": "2014-02-21T00:00:00+00:00", + "resourceType": "MedicationStatement", + "status": "active" + } + }, + { + "resource": { + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "322280009", + "display": "paracetamol 500 milligram/1 each conventional release oral capsule " + } + ] + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Medication-1" + ] + }, + "id": "9294f77d-da2e-4051-bf42-b1baaf714139", + "resourceType": "Medication" + } + }, + { + "resource": { + "mode": "snapshot", + "date": "2026-01-23T11:00:18+00:00", + "entry": [ + { + "item": { + "reference": "MedicationStatement/887ead30-86a8-47c7-9735-a072b10a9549" + } + }, + { + "item": { + "reference": "MedicationStatement/b15c4ad2-c28f-4ea9-984a-03b116d09ac7" + } + }, + { + "item": { + "reference": "MedicationStatement/55d06ed3-6643-44c5-ab15-6015d0683dfd" + } + }, + { + "item": { + "reference": "MedicationStatement/cc6bc75d-3c1b-48cb-b1be-ffba0d93d3a5" + } + }, + { + "item": { + "reference": "MedicationStatement/1d845585-400f-4a10-b8a3-b31dd4dd4621" + } + }, + { + "item": { + "reference": "MedicationStatement/8bd34353-6d29-42fa-93e4-d64d34b13f2c" + } + }, + { + "item": { + "reference": "MedicationStatement/643a0883-efbc-4c49-9695-31a2442e88f3" + } + } + ], + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-List-1" + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "title": "Medication List", + "resourceType": "List", + "status": "current" + } + }, + { + "resource": { + "statusHistory": [ + { + "period": { + "start": "2013-05-29T00:00:00+00:00" + }, + "status": "planned" + } + ], + "period": { + "start": "2013-05-29T00:00:00+00:00" + }, + "managingOrganization": { + "reference": "Organization/5ff06392-92cb-4e43-a4cf-d7d683d09197" + }, + "meta": { + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "D82027", + "display": "HEACHAM GROUP PRACTICE" + } + ] + }, + "patient": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "careManager": { + "reference": "Practitioner/75bc169c-df60-41d5-9782-5e785529eb40" + }, + "id": "432cc9ff-57ac-4ee4-a549-806afc1e7e8f", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/episodeofcare-type", + "code": "R", + "display": "Regular/GMS" + } + ] + } + ], + "resourceType": "EpisodeOfCare", + "status": "active" + } + }, + { + "resource": { + "statusHistory": [ + { + "period": { + "start": "2013-11-29T00:00:00+00:00" + }, + "status": "planned" + } + ], + "period": { + "start": "2013-11-29T00:00:00+00:00" + }, + "managingOrganization": { + "reference": "Organization/5ff06392-92cb-4e43-a4cf-d7d683d09197" + }, + "meta": { + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "patient": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "careManager": { + "reference": "Practitioner/75bc169c-df60-41d5-9782-5e785529eb40" + }, + "id": "e987c75f-148c-4053-9322-275badea0849", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/episodeofcare-type", + "code": "R", + "display": "Regular/GMS" + } + ] + } + ], + "resourceType": "EpisodeOfCare", + "status": "active" + } + }, + { + "resource": { + "date": "2014-11-04T00:00:00+00:00", + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73958" + } + ], + "primarySource": true, + "extension": [ + { + "valueDateTime": "2014-11-04T00:00:00+00:00", + "url": "https://fhir.nhs.uk/STU3/StructureDefinition/Extension-CareConnect-GPC-DateRecorded-1" + }, + { + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "65ED.", + "display": "Seasonal influenza vaccination" + } + ] + }, + "url": null + } + ], + "practitioner": [ + { + "actor": { + "reference": "Practitioner/0bced123-a89d-4ba6-9dc3-03723415aef7" + } + } + ], + "meta": { + "profile": [ + "https://fhir.nhs.uk/STU3/StructureDefinition/CareConnect-GPC-Immunization-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "patient": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "d6575ce2-03ff-4f2d-b5eb-f097b7d188a1", + "explanation": { + "reason": [ + { + "coding": [ + { + "system": "http://snomed.info/sct" + } + ] + } + ] + }, + "resourceType": "Immunization", + "status": "completed" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73562" + } + ], + "specialty": { + "coding": [ + { + "system": "http://orionhealth.com/fhir/apps/specialties", + "code": "8HC..", + "display": "Refer to hospital casualty" + } + ] + }, + "authoredOn": "2014-09-17T00:00:00+00:00", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ReferralRequest-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "977d6f4e-37bc-4486-b225-f6681667bfbc", + "reasonCode": [ + { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "M", + "display": "Management advice" + } + ] + } + ], + "priority": "routine", + "intent": "order", + "resourceType": "ReferralRequest", + "status": "active" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73563" + } + ], + "specialty": { + "coding": [ + { + "system": "http://orionhealth.com/fhir/apps/specialties", + "code": "8H7..", + "display": "Other referral" + } + ] + }, + "authoredOn": "2013-12-20T00:00:00+00:00", + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-ReferralRequest-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "ffb3310e-8330-447d-ac47-4d861de86fc9", + "resourceType": "ReferralRequest", + "status": "active" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73624" + } + ], + "code": { + "coding": [ + { + "extension": [ + { + "valueString": "O/E - weight", + "url": "descriptionDisplay" + } + ], + "system": "http://snomed.info/sct", + "code": "22A..", + "display": "O/E - weight" + } + ] + }, + "performer": [ + { + "reference": "PractitionerRole/6b80ffb7-0b08-4550-99a8-d3cb1d35e10d" + } + ], + "effectivePeriod": { + "start": "2014-02-03T00:00:00+00:00" + }, + "meta": { + "profile": [ + "https://fhir.nhs.uk/STU3/StructureDefinition/CareConnect-GPC-Observation-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "fe95ca98-d2f7-4926-9adb-0884bd885f1d", + "category": [ + { + "text": "vital-signs" + } + ], + "resourceType": "Observation", + "status": "final", + "valueQuantity": { + "unit": "kg", + "system": "http://unitsofmeasure.org", + "value": 101.606 + } + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73663" + } + ], + "code": { + "coding": [ + { + "extension": [ + { + "valueString": "O/E - Systolic BP reading", + "url": "descriptionDisplay" + } + ], + "system": "http://snomed.info/sct", + "code": "2469.", + "display": "O/E - Systolic BP reading" + } + ] + }, + "performer": [ + { + "reference": "PractitionerRole/a5357357-509f-4b85-94c3-de3edf6f35f3" + } + ], + "effectivePeriod": { + "start": "2013-12-20T00:00:00+00:00" + }, + "meta": { + "profile": [ + "https://fhir.nhs.uk/STU3/StructureDefinition/CareConnect-GPC-Observation-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "cf42ee05-ae30-4836-9bc7-7395badce9f8", + "category": [ + { + "text": "vital-signs" + } + ], + "resourceType": "Observation", + "status": "final", + "valueQuantity": { + "unit": "mmHg", + "system": "http://unitsofmeasure.org", + "value": 111.0 + } + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73667" + } + ], + "code": { + "coding": [ + { + "extension": [ + { + "valueString": "O/E - Diastolic BP reading", + "url": "descriptionDisplay" + } + ], + "system": "http://snomed.info/sct", + "code": "246A.", + "display": "O/E - Diastolic BP reading" + } + ] + }, + "performer": [ + { + "reference": "PractitionerRole/a5357357-509f-4b85-94c3-de3edf6f35f3" + } + ], + "effectivePeriod": { + "start": "2013-12-20T00:00:00+00:00" + }, + "meta": { + "profile": [ + "https://fhir.nhs.uk/STU3/StructureDefinition/CareConnect-GPC-Observation-1" + ], + "tag": [ + { + "system": "https://fhir.nhs.uk/Id/ODS-Code", + "code": "EMIS99", + "display": "GPES Org 20077" + } + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "id": "a1a2515d-0a6a-4635-a59f-2fed6831c030", + "category": [ + { + "text": "vital-signs" + } + ], + "resourceType": "Observation", + "status": "final", + "valueQuantity": { + "unit": "mmHg", + "system": "http://unitsofmeasure.org", + "value": 75.0 + } + } + }, + { + "resource": { + "mode": "snapshot", + "date": "2026-01-23T11:00:18+00:00", + "entry": [ + { + "item": { + "reference": "Observation/fe95ca98-d2f7-4926-9adb-0884bd885f1d" + } + }, + { + "item": { + "reference": "Observation/cf42ee05-ae30-4836-9bc7-7395badce9f8" + } + }, + { + "item": { + "reference": "Observation/a1a2515d-0a6a-4635-a59f-2fed6831c030" + } + } + ], + "orderedBy": { + "coding": [ + { + "system": "http://hl7.org/fhir/list-order", + "code": "event-date" + } + ] + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-List-1" + ] + }, + "subject": { + "reference": "Patient/bf3904da-c11f-4004-a774-f6049cb8308e" + }, + "title": "Miscellaneous record", + "resourceType": "List", + "status": "current" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "D82027" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "1" + } + ], + "address": [ + { + "postalCode": "PE31 7EX" + } + ], + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Organization-1" + ] + }, + "name": "HEACHAM GROUP PRACTICE", + "id": "5ff06392-92cb-4e43-a4cf-d7d683d09197", + "resourceType": "Organization" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/ods-organization-code", + "value": "EMIS99" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "14089" + } + ], + "address": [ + { + "postalCode": "LS299EN" + } + ], + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Organization-1" + ] + }, + "name": "GPES Org 20077", + "id": "e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d", + "resourceType": "Organization" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73891" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.501+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "Lina" + ], + "use": "usual", + "prefix": [ + "Dr" + ], + "family": "LAWRENS" + } + ], + "id": "f21dc34a-5d6d-4b86-b1ff-83521094bea7", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73891" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/f21dc34a-5d6d-4b86-b1ff-83521094bea7" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "a5357357-509f-4b85-94c3-de3edf6f35f3", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73959" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.484+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "david" + ], + "use": "usual", + "prefix": [ + "Dr" + ], + "family": "BINNEY" + } + ], + "id": "feaa62a1-f743-4b45-b686-6d29e1953dcc", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73959" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/feaa62a1-f743-4b45-b686-6d29e1953dcc" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "717b7638-2b37-49eb-83de-4e98a4dfcb73", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "871" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.457+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "david" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "BINNEY" + } + ], + "id": "75bc169c-df60-41d5-9782-5e785529eb40", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "871" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/75bc169c-df60-41d5-9782-5e785529eb40" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "id": "9dd7249c-b05a-47ec-a24e-a8ef0d88d39f", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "15306" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.483+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "david" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "BINNEY" + } + ], + "id": "6ab29e2c-c1b9-4fd6-b878-9a8f5435da0f", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "15306" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/6ab29e2c-c1b9-4fd6-b878-9a8f5435da0f" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "272d8751-f659-4263-b12e-f6c996a6e19b", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "39822" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.492+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "use": "usual", + "family": "System User" + } + ], + "id": "0bced123-a89d-4ba6-9dc3-03723415aef7", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "39822" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R5007", + "display": "System Administrator" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/0bced123-a89d-4ba6-9dc3-03723415aef7" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "id": "cf820291-a019-41a1-b6c6-8e547b45ef31", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "15951" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.476+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "Andy" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "MARSHALL SEAS" + } + ], + "id": "eb7064c6-c774-41fe-9d37-ef6c15bb2f9e", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "15951" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/eb7064c6-c774-41fe-9d37-ef6c15bb2f9e" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "0256ac41-0938-4608-a9ea-1826c6593194", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "65202" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.476+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "Richard" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "HAWLEY" + } + ], + "id": "1fe78207-9dbd-4d8c-9336-9f717fa4445c", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "65202" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/1fe78207-9dbd-4d8c-9336-9f717fa4445c" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "66d19ac6-ce1b-4623-b860-372634dca807", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73972" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.483+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "Martin" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "CAIN" + } + ], + "id": "c806a152-60b4-47fb-8aaf-d54aacef3596", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73972" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/c806a152-60b4-47fb-8aaf-d54aacef3596" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "308d34a6-23b4-48c2-bbf0-38ba493c91e9", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73974" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.498+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "Craig" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "TURNER" + } + ], + "id": "374e8979-2c5a-4c11-a871-9386b07c9373", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73974" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/374e8979-2c5a-4c11-a871-9386b07c9373" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "6b80ffb7-0b08-4550-99a8-d3cb1d35e10d", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73881" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.481+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "ryan" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "WALL" + } + ], + "id": "cd5e6337-65a7-436a-aeb0-f4431b3f4086", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73881" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/cd5e6337-65a7-436a-aeb0-f4431b3f4086" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "c0b86203-a8de-457e-b893-964d4bd558a6", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73880" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.475+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "Christopher" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "ROBERTS" + } + ], + "id": "e2e37d82-b709-4df2-b8c1-a63dcfe1837d", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73880" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/e2e37d82-b709-4df2-b8c1-a63dcfe1837d" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "89adffa3-0342-407e-9f65-4f2ff39cfebf", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73976" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.484+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "Peter" + ], + "use": "usual", + "prefix": [ + "Dr" + ], + "family": "ROHAT" + } + ], + "id": "f5822658-f24c-4620-94b8-efd4ba66c850", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73976" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/f5822658-f24c-4620-94b8-efd4ba66c850" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "ce01a137-45e1-4142-84e2-0d16263feacc", + "resourceType": "PractitionerRole" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.nhs.uk/Id/sds-user-id" + }, + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73917" + } + ], + "meta": { + "lastUpdated": "2026-01-23T11:00:18.482+00:00", + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-Practitioner-1" + ] + }, + "name": [ + { + "given": [ + "David" + ], + "use": "usual", + "prefix": [ + "Mr" + ], + "family": "BINNEY" + } + ], + "id": "c45db0a4-b488-4435-aa06-f44dead35a63", + "resourceType": "Practitioner" + } + }, + { + "resource": { + "identifier": [ + { + "system": "https://fhir.hl7.org.uk/Id/dds", + "value": "73917" + } + ], + "code": [ + { + "coding": [ + { + "system": "https://fhir.hl7.org.uk/STU3/CodeSystem/CareConnect-SDSJobRoleName-1", + "code": "R0260", + "display": "General Medical Practitioner" + } + ] + } + ], + "practitioner": { + "reference": "Practitioner/c45db0a4-b488-4435-aa06-f44dead35a63" + }, + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-PractitionerRole-1" + ] + }, + "organization": { + "reference": "Organization/e8cbeaa1-9ef0-4c0e-9d36-7c942c78ca8d" + }, + "id": "a23bb040-3d51-47dd-a9cc-a853db62e860", + "resourceType": "PractitionerRole" + } + } + ], + "meta": { + "profile": [ + "https://fhir.hl7.org.uk/STU3/StructureDefinition/CareConnect-StructuredRecord-Bundle-1" + ] + }, + "type": "collection", + "resourceType": "Bundle" +} \ No newline at end of file diff --git a/reactapp1.client/src/types/fhir.tsx b/reactapp1.client/src/types/fhir.tsx new file mode 100644 index 0000000..1e7a4e4 --- /dev/null +++ b/reactapp1.client/src/types/fhir.tsx @@ -0,0 +1,3 @@ +export type PatientType = { + +} \ No newline at end of file diff --git a/reactapp1.client/src/viewer.jsx b/reactapp1.client/src/viewer.jsx new file mode 100644 index 0000000..cef4a05 --- /dev/null +++ b/reactapp1.client/src/viewer.jsx @@ -0,0 +1,28 @@ +import PatientView from "./PatientView"; +import { patient } from "./patient"; + + +function Viewer() { + const resourceTypes = patient.entry.map(x => x.resource.resourceType); + const counts = resourceTypes.reduce((acc, item) => { + acc[item] = (acc[item] || 0) + 1; + return acc; + }, {}) + + const patientEntry = patient.entry.filter(x => { + if (x.resource) + return x.resource.resourceType == "Patient"; + }); + + return <> + {Object.entries(counts).map(([k, v]) => { + return
{k} : {v}
+ })} +
+            
+        
+ + +} + +export default Viewer; diff --git a/reactapp1.client/vite.config.js b/reactapp1.client/vite.config.js new file mode 100644 index 0000000..03bbcae --- /dev/null +++ b/reactapp1.client/vite.config.js @@ -0,0 +1,87 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { env } from 'process'; +import { fileURLToPath, URL } from 'node:url'; +import fs from 'fs'; +import path from 'path'; +import child_process from 'child_process'; + + +const baseFolder = + env.APPDATA !== undefined && env.APPDATA !== '' + ? `${env.APPDATA}/ASP.NET/https` + : `${env.HOME}/.aspnet/https`; +console.log(baseFolder); +const certificateName = "aspnetapp"; +const certFilePath = path.join(baseFolder, `${certificateName}.pem`); +const keyFilePath = path.join(baseFolder, `${certificateName}.key`); + +if (!fs.existsSync(baseFolder)) { + fs.mkdirSync(baseFolder, { recursive: true }); +} + +if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { + if (0 !== child_process.spawnSync('dotnet', [ + 'dev-certs', + 'https', + '--export-path', + certFilePath, + '--format', + 'Pem', + '--no-password', + ], { stdio: 'inherit', }).status) { + throw new Error("Could not create certificate."); + } +} + + +const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` : + env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7284'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + proxy: { + '^/WeatherForecast': { + target, + secure: false + }, + '^/login': { + target, + secure: false + }, + '^/callback': { + target, + secure: false + }, + '^/logout': { + target, + secure: false + }, + '/debug/tokens': { + target, + secure: false + }, + '/auth/': { + target, + secure: false + }, + + '/api/': { + target, + secure: false + } + }, + port: 5174, + https: { + key: fs.readFileSync(keyFilePath), + cert: fs.readFileSync(certFilePath), + } + } +}) From 98aa9cdb646fbccbc88a615a904d12e3a0a7de56 Mon Sep 17 00:00:00 2001 From: davidhayes03 Date: Wed, 18 Mar 2026 12:23:29 +0000 Subject: [PATCH 2/4] CODE RUB: Update to run and return --- .../NhsLoginTests.BuildLoginUrl.cs | 21 ++++++++++ .../NhsLoginTests.GetAccessToken.cs | 20 ++++++++++ .../NhsLoginTests.GetUserInfo.cs | 27 +++++++++++++ .../NhsLoginTests.Logout.cs | 20 ++++++++++ .../NhsLoginTests.cs | 38 +++++++++++++++++++ .../appsettings.json | 11 ++++++ .../NHS.Digital.ApiPlatform.Sdk.csproj | 1 - .../ServiceCollectionExtensions.cs | 33 ++++++++++------ .../Services/Foundations/Pds/PdsService.cs | 4 +- .../Controllers/AuthController.cs | 2 +- ReactApp1.Server/appsettings.json | 18 +++++++++ 11 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.BuildLoginUrl.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.GetAccessToken.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.GetUserInfo.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.Logout.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.cs diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.BuildLoginUrl.cs b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.BuildLoginUrl.cs new file mode 100644 index 0000000..ba30676 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.BuildLoginUrl.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace NHS.Digital.ApiPlatform.Sdk.Tests.Integration +{ + public partial class NhsLoginTests + { + [Fact] + public async Task BuildLoginUrl() + { + // given + // when + string loginUrl = await careIdentityServiceClient.BuildLoginUrlAsync(); + + // then + Assert.False(string.IsNullOrWhiteSpace(loginUrl), "Login URL should not be null or empty."); + Assert.Contains(apiPlatformConfigurations.CareIdentity.AuthEndpoint, loginUrl); + } + } +} \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.GetAccessToken.cs b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.GetAccessToken.cs new file mode 100644 index 0000000..24cf983 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.GetAccessToken.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace NHS.Digital.ApiPlatform.Sdk.Tests.Integration +{ + public partial class NhsLoginTests + { + [Fact] + public async Task GetAccessToken() + { + // given + // when + await careIdentityServiceClient.GetAccessTokenAsync(); + + // then + Assert.True(true, "Logout completed successfully without throwing an exception."); + } + } +} \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.GetUserInfo.cs b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.GetUserInfo.cs new file mode 100644 index 0000000..314e85c --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.GetUserInfo.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.CareIdentityServices; +using Xunit; + +namespace NHS.Digital.ApiPlatform.Sdk.Tests.Integration +{ + public partial class NhsLoginTests + { + [Fact(Skip = "Requires real NHS authentication flow with valid authorization code")] + public async Task GetUserInfo() + { + // given + string code = "test-authorization-code"; + string state = "test-state-value"; + + // when + NhsUserInfo userInfo = + await careIdentityServiceClient.GetUserInfoAsync( + code, + state); + + // then + Assert.NotNull(userInfo); + } + } +} \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.Logout.cs b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.Logout.cs new file mode 100644 index 0000000..0b70140 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.Logout.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace NHS.Digital.ApiPlatform.Sdk.Tests.Integration +{ + public partial class NhsLoginTests + { + [Fact] + public async Task Logout() + { + // given + // when + await careIdentityServiceClient.LogoutAsync(); + + // then + Assert.True(true, "Logout completed successfully without throwing an exception."); + } + } +} \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.cs b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.cs new file mode 100644 index 0000000..3862d69 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/NhsLoginTests.cs @@ -0,0 +1,38 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using Microsoft.Extensions.Configuration; +using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; +using NHS.Digital.ApiPlatform.Sdk.Clients.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; +using Xunit.Abstractions; + +namespace NHS.Digital.ApiPlatform.Sdk.Tests.Integration +{ + public partial class NhsLoginTests + { + private readonly ICareIdentityServiceClient careIdentityServiceClient; + private readonly ApiPlatformConfigurations apiPlatformConfigurations; + private readonly IConfiguration configuration; + private readonly ITestOutputHelper output; + + public NhsLoginTests(ITestOutputHelper output) + { + this.output = output; + + var configurationBuilder = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + + configuration = configurationBuilder.Build(); + + this.apiPlatformConfigurations = configuration + .GetSection("CIS").Get(); + + var apiPlatformClient = new ApiPlatformClient(this.apiPlatformConfigurations); + this.careIdentityServiceClient = apiPlatformClient.CareIdentityServiceClient; + } + } +} \ No newline at end of file diff --git a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json index 2c63c08..40e7773 100644 --- a/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json @@ -1,2 +1,13 @@ { + "CIS": { + "AuthEndpoint": "https://int.api.service.nhs.uk/oauth2/authorize", + "TokenEndpoint": "https://int.api.service.nhs.uk/oauth2/token", + "UserInfoEndpoint": "https://int.api.service.nhs.uk/oauth2/userinfo", + "LogoutEndpoint": "https://int.api.service.nhs.uk/oauth2/logout", + "PostLogoutRedirectUri": "https://localhost:5174/", + "ClientId": "CsVVAJodqwlRPH479GedNmeCbcWNZ8jW", + "ClientSecret": "HKD8tYgfgFtCf3G0", + "RedirectUri": "https://localhost:5174/auth/callback", + "AALLevel": "AAL2_OR_AAL3_ANY" + } } diff --git a/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj b/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj index f29926d..ae3fc7b 100644 --- a/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj +++ b/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj @@ -4,7 +4,6 @@ net10.0 disable disable - NHS.Digital.ApiPlatform.Sdk NHS.Digital.ApiPlatform.Sdk NHS.Digital.ApiPlatform.Sdk diff --git a/NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs b/NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs index 253b1b9..cfaefee 100644 --- a/NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs +++ b/NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using NHS.Digital.ApiPlatform.Sdk.Brokers.Cryptographies; using NHS.Digital.ApiPlatform.Sdk.Brokers.DateTimes; using NHS.Digital.ApiPlatform.Sdk.Brokers.Https; +using NHS.Digital.ApiPlatform.Sdk.Brokers.Identifiers; using NHS.Digital.ApiPlatform.Sdk.Brokers.Serializations; using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; @@ -14,6 +15,8 @@ using NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices; using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices; +using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds; +using NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds; using NHS.Digital.ApiPlatform.Sdk.Services.Processings.CareIdentityServices; namespace NHS.Digital.ApiPlatform.Sdk @@ -24,19 +27,25 @@ public static IServiceCollection AddApiPlatformSdkCore( this IServiceCollection services, ApiPlatformConfigurations apiPlatformConfigurations) { - services.AddSingleton(apiPlatformConfigurations); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddHttpClient("NhsApiPlatform"); - services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.TryAddTransient(); + services.AddSingleton(apiPlatformConfigurations); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHttpClient("NhsApiPlatform"); + services.AddTransient(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + services.TryAddTransient(serviceProvider => + new ApiPlatformClient( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService())); - return services; + return services; } public static IServiceCollection AddApiPlatformSdkInMemoryStorage(this IServiceCollection services) diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs index 65a29c9..f51fa94 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs @@ -22,11 +22,11 @@ internal partial class PdsService : IPdsService private readonly IIdentifierBroker identifierBroker; public PdsService( - IOptions configurations, + ApiPlatformConfigurations configurations, IHttpBroker httpBroker, IIdentifierBroker identifierBroker) { - this.configurations = configurations.Value; + this.configurations = configurations; this.httpBroker = httpBroker; this.identifierBroker = identifierBroker; } diff --git a/ReactApp1.Server/Controllers/AuthController.cs b/ReactApp1.Server/Controllers/AuthController.cs index 57608c2..f6fad5f 100644 --- a/ReactApp1.Server/Controllers/AuthController.cs +++ b/ReactApp1.Server/Controllers/AuthController.cs @@ -82,7 +82,7 @@ await this.apiPlatformClient HttpContext.Session.Clear(); await HttpContext.SignOutAsync("bff-cookie"); - return Redirect(@""); + return Redirect(@"\"); } [HttpGet("callback")] diff --git a/ReactApp1.Server/appsettings.json b/ReactApp1.Server/appsettings.json index 49a3340..d52e54e 100644 --- a/ReactApp1.Server/appsettings.json +++ b/ReactApp1.Server/appsettings.json @@ -7,5 +7,23 @@ "Microsoft.IdentityModel": "Trace" } }, + "ConnectionStrings": { + "iDecide": "Server=(localdb)\\MSSQLLocalDB;Database=IDecide;Trusted_Connection=True;MultipleActiveResultSets=true", + "SessionCache": "Server=(localdb)\\MSSQLLocalDB;Database=IDecide;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "CIS": { + "AuthEndpoint": "https://int.api.service.nhs.uk/oauth2/authorize", + "TokenEndpoint": "https://int.api.service.nhs.uk/oauth2/token", + "UserInfoEndpoint": "https://int.api.service.nhs.uk/oauth2/userinfo", + "LogoutEndpoint": "https://int.api.service.nhs.uk/oauth2/logout", + "PostLogoutRedirectUri": "https://localhost:5174/", + "ClientId": "CsVVAJodqwlRPH479GedNmeCbcWNZ8jW", + "ClientSecret": "HKD8tYgfgFtCf3G0", + "RedirectUri": "https://localhost:5174/auth/callback", + "AALLevel": "AAL2_OR_AAL3_ANY" + }, + "PDS": { + "BaseUrl": "https://int.api.service.nhs.uk/personal-demographics/FHIR/R4" + }, "AllowedHosts": "*" } From 9d40f445d32a4ceca82302bdfbad026590be848e Mon Sep 17 00:00:00 2001 From: davidhayes03 Date: Wed, 18 Mar 2026 15:19:19 +0000 Subject: [PATCH 3/4] CODE RUB: Pass Patient Model in --- .../IPersonalDemographicsServiceClient.cs | 6 +-- .../PersonalDemographicsServiceClient.cs | 11 ++-- .../Models/Foundations/Patients/Address.cs | 17 +++++++ .../Models/Foundations/Patients/Patient.cs | 51 +++++++++++++++++++ .../Services/Foundations/Pds/IPdsService.cs | 6 +-- .../Services/Foundations/Pds/PdsService.cs | 35 ++++++++----- .../Pds/IPdsOrchestrationService.cs | 6 +-- .../Pds/PdsOrchestrationService.cs | 10 ++-- .../Controllers/PatientController.cs | 29 ++++++----- 9 files changed, 119 insertions(+), 52 deletions(-) create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Address.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs index 6bb95df..56dbab5 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs @@ -6,16 +6,14 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; namespace NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices { public interface IPersonalDemographicsServiceClient { ValueTask SearchPatientsAsync( - string family, - IEnumerable? given = null, - string? gender = null, - DateOnly? birthdate = null, + Patient patient, CancellationToken cancellationToken = default); } } diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs index 4eaab79..588469b 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using NHS.Digital.ApiPlatform.Sdk.Models.Clients.Pds.Exceptions; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; using NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions; using NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds; using Xeptions; @@ -21,19 +22,13 @@ public PersonalDemographicsServiceClient(IPdsOrchestrationService pdsOrchestrati this.pdsOrchestrationService = pdsOrchestrationService; public async ValueTask SearchPatientsAsync( - string family, - IEnumerable given = null, - string gender = null, - DateOnly? birthdate = null, + Patient patient, CancellationToken cancellationToken = default) { try { return await this.pdsOrchestrationService.SearchPatientsAsync( - family, - given, - gender, - birthdate, + patient, cancellationToken); } catch (PdsOrchestrationValidationException pdsOrchestrationValidationException) diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Address.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Address.cs new file mode 100644 index 0000000..1f6e3f7 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Address.cs @@ -0,0 +1,17 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients +{ + public class Address + { + public string RecipientName { get; set; } + public string AddressLine1 { get; set; } + public string AddressLine2 { get; set; } + public string AddressLine3 { get; set; } + public string AddressLine4 { get; set; } + public string AddressLine5 { get; set; } + public string PostCode { get; set; } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs new file mode 100644 index 0000000..21a1b5f --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs @@ -0,0 +1,51 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Net; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients +{ + public class Patient + { + public string NhsNumber { get; set; } + public string Title { get; set; } + public IEnumerable GivenName { get; set; } + public string Surname { get; set; } + public DateTimeOffset DateOfBirth { get; set; } + public string Gender { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string Address { get; set; } + public string PostCode { get; set; } + + [NotMapped] + public Address PostalAddress + { + get + { + var addressLines = (Address ?? string.Empty).Split(','); + var addressLine1 = addressLines.ElementAtOrDefault(0) ?? string.Empty; + var addressLine2 = addressLines.ElementAtOrDefault(1) ?? string.Empty; + var addressLine3 = addressLines.ElementAtOrDefault(2) ?? string.Empty; + var addressLine4 = addressLines.ElementAtOrDefault(3) ?? string.Empty; + var addressLine5 = addressLines.ElementAtOrDefault(4) ?? string.Empty; + + return new Address + { + RecipientName = $"{Title} {GivenName} {Surname}", + AddressLine1 = addressLine1, + AddressLine2 = addressLine2, + AddressLine3 = addressLine3, + AddressLine4 = addressLine4, + AddressLine5 = addressLine5, + PostCode = PostCode + }; + } + } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs index 83d8ca7..4dbb028 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds { @@ -13,10 +14,7 @@ internal interface IPdsService { ValueTask SearchPatientsAsync( string accessToken, - string family, - IEnumerable? given = null, - string? gender = null, - DateOnly? birthdate = null, + Patient patient, CancellationToken cancellationToken = default); } } diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs index f51fa94..2a7dad4 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs @@ -12,6 +12,7 @@ using NHS.Digital.ApiPlatform.Sdk.Brokers.Https; using NHS.Digital.ApiPlatform.Sdk.Brokers.Identifiers; using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds { @@ -32,33 +33,39 @@ public PdsService( } public ValueTask SearchPatientsAsync( - string accessToken, - string family, - IEnumerable given = null, - string gender = null, - DateOnly? birthdate = null, - CancellationToken cancellationToken = default) => + string accessToken, + Patient patient, + CancellationToken cancellationToken = default) => TryCatch(async () => { string baseUrl = this.configurations.PersonalDemographicsService.BaseUrl.TrimEnd('/'); - string url = $"{baseUrl}/Patient?family={Uri.EscapeDataString(family)}"; + string url; - if (given is not null) + if (!string.IsNullOrWhiteSpace(patient.NhsNumber)) { - foreach (string givenName in given.Where(n => !string.IsNullOrWhiteSpace(n))) + url = $"{baseUrl}/Patient/{patient.NhsNumber}"; + } + else + { + url = $"{baseUrl}/Patient?family={Uri.EscapeDataString(patient.Surname)}"; + + if (patient.GivenName is not null) + { + foreach (string givenName in patient.GivenName.Where(n => !string.IsNullOrWhiteSpace(n))) { url += $"&given={Uri.EscapeDataString(givenName)}"; } } - if (string.IsNullOrWhiteSpace(gender) is false) + if (!string.IsNullOrWhiteSpace(patient.Gender)) { - url += $"&gender={Uri.EscapeDataString(gender)}"; + url += $"&gender={Uri.EscapeDataString(patient.Gender)}"; } - if (birthdate is not null) - { - url += $"&birthdate=eq{birthdate:yyyy-MM-dd}"; + if (patient.DateOfBirth != default) + { + url += $"&birthdate=eq{patient.DateOfBirth:yyyy-MM-dd}"; + } } var response = await this.httpBroker.GetAsync( diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs index 1e3d75f..05a3ae2 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs @@ -5,16 +5,14 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; namespace NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds { public interface IPdsOrchestrationService { ValueTask SearchPatientsAsync( - string family, - IEnumerable? given = null, - string? gender = null, - DateOnly? birthdate = null, + Patient patient, CancellationToken cancellationToken = default); } } diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs index 1559aa9..8e8bac9 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; using NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions; using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices; using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds; @@ -26,14 +27,11 @@ public PdsOrchestrationService(ICareIdentityService careIdentityService, IPdsSer } public ValueTask SearchPatientsAsync( - string family, - IEnumerable given = null, - string gender = null, - DateOnly? birthdate = null, + Patient patient, CancellationToken cancellationToken = default) => TryCatch(async () => { - ValidateOnSearchPatientsAsync(family, given, gender, birthdate); + //ValidateOnSearchPatientsAsync(family, given, gender, birthdate); string accessToken = await this.careIdentityService.GetAccessTokenAsync(cancellationToken); if (string.IsNullOrWhiteSpace(accessToken)) @@ -42,7 +40,7 @@ public ValueTask SearchPatientsAsync( } return await this.pdsService - .SearchPatientsAsync(accessToken, family, given, gender, birthdate, cancellationToken); + .SearchPatientsAsync(accessToken, patient, cancellationToken); }); } } diff --git a/ReactApp1.Server/Controllers/PatientController.cs b/ReactApp1.Server/Controllers/PatientController.cs index 56f7e1c..e1b72bb 100644 --- a/ReactApp1.Server/Controllers/PatientController.cs +++ b/ReactApp1.Server/Controllers/PatientController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; namespace ReactApp1.Server.Controllers; @@ -23,23 +24,27 @@ public async Task GetPatient(CancellationToken cancellationToken) { try { - string body = await this.apiPlatformClient - .PersonalDemographicsServiceClient - .SearchPatientsAsync( - family: "Smith", - given: null, - gender: "female", - birthdate: new DateOnly(2010, 10, 22), - cancellationToken: cancellationToken); + var patient = new Patient + { + NhsNumber = "9000000009", + Surname = "Smith", + GivenName = null, + Gender = "female", + DateOfBirth = new DateTimeOffset(new DateOnly(2010, 10, 22), TimeOnly.MinValue, TimeSpan.Zero) + }; - return Content(body, "application/fhir+json"); - } + string body = await this.apiPlatformClient + .PersonalDemographicsServiceClient + .SearchPatientsAsync( + patient, + cancellationToken: cancellationToken); + + return Content(body, "application/fhir+json"); + } catch (Exception exception) { this.logger.LogError(exception, "Error while searching for patients."); - // If token is missing/expired you will typically see an unauthorized exception - // bubble up from the SDK. Return 401 as the most useful default. return Unauthorized(); } } From 158f1584aed6ffc6bc79455fd4eebe3eb5f66d56 Mon Sep 17 00:00:00 2001 From: davidhayes03 Date: Thu, 19 Mar 2026 13:44:12 +0000 Subject: [PATCH 4/4] CODE RUB: Pass in PatientSearchCrieria --- .../IPersonalDemographicsServiceClient.cs | 3 +- .../PersonalDemographicsServiceClient.cs | 5 +- .../Models/Foundations/Patients/Patient.cs | 19 +++- .../Models/Foundations/Pds/PatientLookup.cs | 15 +++ .../Models/Foundations/Pds/SearchCriteria.cs | 20 ++++ .../NHS.Digital.ApiPlatform.Sdk.csproj | 11 ++- .../Services/Foundations/Pds/IPdsService.cs | 3 +- .../Services/Foundations/Pds/PdsService.cs | 91 +++++++++---------- .../Pds/IPdsOrchestrationService.cs | 3 +- .../Pds/PdsOrchestrationService.cs | 6 +- .../Controllers/PatientController.cs | 59 ++++++------ 11 files changed, 147 insertions(+), 88 deletions(-) create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/PatientLookup.cs create mode 100644 NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/SearchCriteria.cs diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs index 56dbab5..81f2f44 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.cs @@ -7,13 +7,14 @@ using System.Threading; using System.Threading.Tasks; using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; namespace NHS.Digital.ApiPlatform.Sdk.Clients.PersonalDemographicsServices { public interface IPersonalDemographicsServiceClient { ValueTask SearchPatientsAsync( - Patient patient, + SearchCriteria searchCriteria, CancellationToken cancellationToken = default); } } diff --git a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs index 588469b..e09975d 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using NHS.Digital.ApiPlatform.Sdk.Models.Clients.Pds.Exceptions; using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; using NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions; using NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds; using Xeptions; @@ -22,13 +23,13 @@ public PersonalDemographicsServiceClient(IPdsOrchestrationService pdsOrchestrati this.pdsOrchestrationService = pdsOrchestrationService; public async ValueTask SearchPatientsAsync( - Patient patient, + SearchCriteria searchCriteria, CancellationToken cancellationToken = default) { try { return await this.pdsOrchestrationService.SearchPatientsAsync( - patient, + searchCriteria, cancellationToken); } catch (PdsOrchestrationValidationException pdsOrchestrationValidationException) diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs index 21a1b5f..53e730b 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs @@ -7,6 +7,8 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Net; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients { @@ -14,7 +16,7 @@ public class Patient { public string NhsNumber { get; set; } public string Title { get; set; } - public IEnumerable GivenName { get; set; } + public string GivenName { get; set; } public string Surname { get; set; } public DateTimeOffset DateOfBirth { get; set; } public string Gender { get; set; } @@ -47,5 +49,20 @@ public Address PostalAddress }; } } + + public string ValidationCode { get; set; } + public DateTimeOffset ValidationCodeExpiresOn { get; set; } + public DateTimeOffset? ValidationCodeMatchedOn { get; set; } + public int RetryCount { get; set; } + //public NotificationPreference NotificationPreference { get; set; } + public string CreatedBy { get; set; } + public DateTimeOffset CreatedDate { get; set; } + public string UpdatedBy { get; set; } + public DateTimeOffset UpdatedDate { get; set; } + + [NotMapped] + [JsonIgnore] + public bool IsSensitive { get; set; } + } } diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/PatientLookup.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/PatientLookup.cs new file mode 100644 index 0000000..a68f86f --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/PatientLookup.cs @@ -0,0 +1,15 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System.Collections.Generic; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds +{ + public class PatientLookup + { + public SearchCriteria SearchCriteria { get; set; } + public List Patients { get; set; } + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/SearchCriteria.cs b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/SearchCriteria.cs new file mode 100644 index 0000000..6a34025 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Pds/SearchCriteria.cs @@ -0,0 +1,20 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds +{ + public class SearchCriteria + { + public string NhsNumber { get; set; } = string.Empty; + public string FirstName { get; set; } = string.Empty; + public string Surname { get; set; } = string.Empty; + public string Gender { get; set; } = string.Empty; + public string Postcode { get; set; } = string.Empty; + public string DateOfBirth { get; set; } = string.Empty; + public string DateOfDeath { get; set; } = string.Empty; + public string RegisteredGpPractice { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; + } +} diff --git a/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj b/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj index ae3fc7b..6799a24 100644 --- a/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj +++ b/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj @@ -56,9 +56,12 @@ - - - + + + + + + - + diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs index 4dbb028..64f8a18 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds { @@ -14,7 +15,7 @@ internal interface IPdsService { ValueTask SearchPatientsAsync( string accessToken, - Patient patient, + SearchCriteria searchCriteria, CancellationToken cancellationToken = default); } } diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs index 2a7dad4..0d0f0a7 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs @@ -3,16 +3,13 @@ // --------------------------------------------------------- using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Options; using NHS.Digital.ApiPlatform.Sdk.Brokers.Https; using NHS.Digital.ApiPlatform.Sdk.Brokers.Identifiers; using NHS.Digital.ApiPlatform.Sdk.Models.Configurations; -using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds { @@ -32,55 +29,57 @@ public PdsService( this.identifierBroker = identifierBroker; } - public ValueTask SearchPatientsAsync( + public ValueTask SearchPatientsAsync( string accessToken, - Patient patient, + SearchCriteria searchCriteria, CancellationToken cancellationToken = default) => - TryCatch(async () => - { - string baseUrl = this.configurations.PersonalDemographicsService.BaseUrl.TrimEnd('/'); - string url; + TryCatch(async () => + { + string baseUrl = this.configurations.PersonalDemographicsService.BaseUrl.TrimEnd('/'); + string url; + + if (!string.IsNullOrWhiteSpace(searchCriteria.NhsNumber)) + { + url = $"{baseUrl}/Patient/{searchCriteria.NhsNumber}"; + } + else + { + url = $"{baseUrl}/Patient?family={Uri.EscapeDataString(searchCriteria.Surname)}"; - if (!string.IsNullOrWhiteSpace(patient.NhsNumber)) - { - url = $"{baseUrl}/Patient/{patient.NhsNumber}"; - } - else - { - url = $"{baseUrl}/Patient?family={Uri.EscapeDataString(patient.Surname)}"; + if (!string.IsNullOrWhiteSpace(searchCriteria.FirstName)) + { + url += $"&given={Uri.EscapeDataString(searchCriteria.FirstName)}"; + } - if (patient.GivenName is not null) - { - foreach (string givenName in patient.GivenName.Where(n => !string.IsNullOrWhiteSpace(n))) - { - url += $"&given={Uri.EscapeDataString(givenName)}"; - } - } + if (!string.IsNullOrWhiteSpace(searchCriteria.Gender)) + { + url += $"&gender={Uri.EscapeDataString(searchCriteria.Gender)}"; + } - if (!string.IsNullOrWhiteSpace(patient.Gender)) - { - url += $"&gender={Uri.EscapeDataString(patient.Gender)}"; - } + if (!string.IsNullOrWhiteSpace(searchCriteria.DateOfBirth)) + { + url += $"&birthdate=eq{searchCriteria.DateOfBirth}"; + } - if (patient.DateOfBirth != default) - { - url += $"&birthdate=eq{patient.DateOfBirth:yyyy-MM-dd}"; - } - } + if (!string.IsNullOrWhiteSpace(searchCriteria.Postcode)) + { + url += $"&postcode={Uri.EscapeDataString(searchCriteria.Postcode)}"; + } + } - var response = await this.httpBroker.GetAsync( - url, - request => - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - request.Headers.Add("X-Request-ID", this.identifierBroker.GetNewGuid().ToString()); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/fhir+json")); - }, - cancellationToken); + var response = await this.httpBroker.GetAsync( + url, + request => + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + request.Headers.Add("X-Request-ID", this.identifierBroker.GetNewGuid().ToString()); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/fhir+json")); + }, + cancellationToken); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(cancellationToken); - }); - } + return await response.Content.ReadAsStringAsync(cancellationToken); + }); + } } diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs index 05a3ae2..fecdcfd 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs @@ -6,13 +6,14 @@ using System.Threading; using System.Threading.Tasks; using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; namespace NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds { public interface IPdsOrchestrationService { ValueTask SearchPatientsAsync( - Patient patient, + SearchCriteria searchCriteria, CancellationToken cancellationToken = default); } } diff --git a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs index 8e8bac9..65021cf 100644 --- a/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using NHS.Digital.ApiPlatform.Sdk.Brokers.Storages; using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; using NHS.Digital.ApiPlatform.Sdk.Models.Orchestrations.Pds.Exceptions; using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.CareIdentityServices; using NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds; @@ -27,11 +28,10 @@ public PdsOrchestrationService(ICareIdentityService careIdentityService, IPdsSer } public ValueTask SearchPatientsAsync( - Patient patient, + SearchCriteria searchCriteria, CancellationToken cancellationToken = default) => TryCatch(async () => { - //ValidateOnSearchPatientsAsync(family, given, gender, birthdate); string accessToken = await this.careIdentityService.GetAccessTokenAsync(cancellationToken); if (string.IsNullOrWhiteSpace(accessToken)) @@ -40,7 +40,7 @@ public ValueTask SearchPatientsAsync( } return await this.pdsService - .SearchPatientsAsync(accessToken, patient, cancellationToken); + .SearchPatientsAsync(accessToken, searchCriteria, cancellationToken); }); } } diff --git a/ReactApp1.Server/Controllers/PatientController.cs b/ReactApp1.Server/Controllers/PatientController.cs index e1b72bb..e9bc8ba 100644 --- a/ReactApp1.Server/Controllers/PatientController.cs +++ b/ReactApp1.Server/Controllers/PatientController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using NHS.Digital.ApiPlatform.Sdk.Clients.ApiPlatforms; using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; namespace ReactApp1.Server.Controllers; @@ -8,44 +9,44 @@ namespace ReactApp1.Server.Controllers; [Route("api/[controller]")] public class PatientController : ControllerBase { - private readonly IApiPlatformClient apiPlatformClient; - private readonly ILogger logger; + private readonly IApiPlatformClient apiPlatformClient; + private readonly ILogger logger; - public PatientController( - IApiPlatformClient apiPlatformClient, - ILogger logger) - { - this.apiPlatformClient = apiPlatformClient; - this.logger = logger; - } + public PatientController( + IApiPlatformClient apiPlatformClient, + ILogger logger) + { + this.apiPlatformClient = apiPlatformClient; + this.logger = logger; + } - [HttpGet] - public async Task GetPatient(CancellationToken cancellationToken) - { - try - { - var patient = new Patient - { - NhsNumber = "9000000009", - Surname = "Smith", - GivenName = null, - Gender = "female", - DateOfBirth = new DateTimeOffset(new DateOnly(2010, 10, 22), TimeOnly.MinValue, TimeSpan.Zero) - }; + [HttpGet] + public async Task GetPatient(CancellationToken cancellationToken) + { + try + { + var searchCriteria = new SearchCriteria + { + NhsNumber = "9000000009", + Surname = "Smith", + FirstName = null, + Gender = "female", + DateOfBirth = "2010-10-22" + }; string body = await this.apiPlatformClient .PersonalDemographicsServiceClient .SearchPatientsAsync( - patient, + searchCriteria, cancellationToken: cancellationToken); return Content(body, "application/fhir+json"); } - catch (Exception exception) - { - this.logger.LogError(exception, "Error while searching for patients."); + catch (Exception exception) + { + this.logger.LogError(exception, "Error while searching for patients."); - return Unauthorized(); - } - } + return Unauthorized(); + } + } }