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/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 new file mode 100644 index 0000000..40e7773 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk.Tests.Integration/appsettings.json @@ -0,0 +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.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..81f2f44 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/IPersonalDemographicsServiceClient.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; +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( + 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 new file mode 100644 index 0000000..e09975d --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Clients/PersonalDemographicsServices/PersonalDemographicsServiceClient.cs @@ -0,0 +1,90 @@ +// --------------------------------------------------------- +// 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.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; + +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( + SearchCriteria searchCriteria, + CancellationToken cancellationToken = default) + { + try + { + return await this.pdsOrchestrationService.SearchPatientsAsync( + searchCriteria, + 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/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..53e730b --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Models/Foundations/Patients/Patient.cs @@ -0,0 +1,68 @@ +// --------------------------------------------------------- +// 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; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients +{ + public class Patient + { + public string NhsNumber { get; set; } + public string Title { get; set; } + public string 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 + }; + } + } + + 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/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/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/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..6799a24 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/NHS.Digital.ApiPlatform.Sdk.csproj @@ -0,0 +1,67 @@ + + + + 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..cfaefee --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/ServiceCollectionExtensions.cs @@ -0,0 +1,60 @@ +// --------------------------------------------------------- +// 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.Identifiers; +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.Foundations.Pds; +using NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds; +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.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; + } + + 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..64f8a18 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/IPdsService.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; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Foundations.Pds +{ + internal interface IPdsService + { + ValueTask SearchPatientsAsync( + string accessToken, + SearchCriteria searchCriteria, + 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..0d0f0a7 --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Foundations/Pds/PdsService.cs @@ -0,0 +1,85 @@ +// --------------------------------------------------------- +// Copyright (c) North East London ICB. All rights reserved. +// --------------------------------------------------------- + +using System; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +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.Pds; + +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( + ApiPlatformConfigurations configurations, + IHttpBroker httpBroker, + IIdentifierBroker identifierBroker) + { + this.configurations = configurations; + this.httpBroker = httpBroker; + this.identifierBroker = identifierBroker; + } + + public ValueTask SearchPatientsAsync( + string accessToken, + SearchCriteria searchCriteria, + CancellationToken cancellationToken = default) => + 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(searchCriteria.FirstName)) + { + url += $"&given={Uri.EscapeDataString(searchCriteria.FirstName)}"; + } + + if (!string.IsNullOrWhiteSpace(searchCriteria.Gender)) + { + url += $"&gender={Uri.EscapeDataString(searchCriteria.Gender)}"; + } + + if (!string.IsNullOrWhiteSpace(searchCriteria.DateOfBirth)) + { + url += $"&birthdate=eq{searchCriteria.DateOfBirth}"; + } + + 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); + + 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..fecdcfd --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/IPdsOrchestrationService.cs @@ -0,0 +1,19 @@ +// --------------------------------------------------------- +// 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.Foundations.Patients; +using NHS.Digital.ApiPlatform.Sdk.Models.Foundations.Pds; + +namespace NHS.Digital.ApiPlatform.Sdk.Services.Orchestrations.Pds +{ + public interface IPdsOrchestrationService + { + ValueTask SearchPatientsAsync( + SearchCriteria searchCriteria, + 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..65021cf --- /dev/null +++ b/NHS.Digital.ApiPlatform.Sdk/Services/Orchestrations/Pds/PdsOrchestrationService.cs @@ -0,0 +1,46 @@ +// --------------------------------------------------------- +// 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.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; + +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( + SearchCriteria searchCriteria, + CancellationToken cancellationToken = default) => + TryCatch(async () => + { + 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, searchCriteria, 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..f6fad5f --- /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..e9bc8ba --- /dev/null +++ b/ReactApp1.Server/Controllers/PatientController.cs @@ -0,0 +1,52 @@ +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; + +[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 + { + var searchCriteria = new SearchCriteria + { + NhsNumber = "9000000009", + Surname = "Smith", + FirstName = null, + Gender = "female", + DateOfBirth = "2010-10-22" + }; + + string body = await this.apiPlatformClient + .PersonalDemographicsServiceClient + .SearchPatientsAsync( + searchCriteria, + cancellationToken: cancellationToken); + + return Content(body, "application/fhir+json"); + } + catch (Exception exception) + { + this.logger.LogError(exception, "Error while searching for patients."); + + 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..d52e54e --- /dev/null +++ b/ReactApp1.Server/appsettings.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.Authentication": "Trace", + "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": "*" +} diff --git a/Resources/Images/NhsDigitalBanner.png b/Resources/Images/NhsDigitalBanner.png new file mode 100644 index 0000000..6eb8dbd Binary files /dev/null and b/Resources/Images/NhsDigitalBanner.png differ diff --git a/Resources/Images/NhsDigitalIcon.png b/Resources/Images/NhsDigitalIcon.png new file mode 100644 index 0000000..8e38ba6 Binary files /dev/null and b/Resources/Images/NhsDigitalIcon.png differ diff --git a/reactapp1.client/.gitignore b/reactapp1.client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/reactapp1.client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/reactapp1.client/CHANGELOG.md b/reactapp1.client/CHANGELOG.md new file mode 100644 index 0000000..04d479b --- /dev/null +++ b/reactapp1.client/CHANGELOG.md @@ -0,0 +1,13 @@ +This file explains how Visual Studio created the project. + +The following tools were used to generate this project: +- create-vite + +The following steps were used to generate this project: +- Create react project with create-vite: `npm init --yes vite@latest reactapp1.client -- --template=react`. +- Create project file (`reactapp1.client.esproj`). +- Create `launch.json` to enable debugging. +- Add project to solution. +- Update proxy endpoint to be the backend server endpoint. +- Add project to the startup projects list. +- Write this file. diff --git a/reactapp1.client/README.md b/reactapp1.client/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/reactapp1.client/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/reactapp1.client/eslint.config.js b/reactapp1.client/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/reactapp1.client/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/reactapp1.client/index.html b/reactapp1.client/index.html new file mode 100644 index 0000000..8eba368 --- /dev/null +++ b/reactapp1.client/index.html @@ -0,0 +1,13 @@ + + + + + + + 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), + } + } +})