diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..81d9de8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,166 @@
+# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
+
+# All files
+[*]
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+; Force VS to recommend underscore at the start of created private fields.
+[*.cs]
+dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
+dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
+dotnet_naming_rule.private_members_with_underscore.severity = suggestion
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private
+
+dotnet_naming_style.prefix_underscore.capitalization = camel_case
+dotnet_naming_style.prefix_underscore.required_prefix = _
+
+# name all constant fields using UpperCase
+dotnet_naming_rule.constant_fields_should_be_upper_case.severity = warning
+dotnet_naming_rule.constant_fields_should_be_upper_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_upper_case.style = upper_case_style
+
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+dotnet_naming_style.upper_case_style.capitalization = pascal_case
+
+; Avoid this. unless absolutely necessary
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+# IDE0024: Use block body for operators
+csharp_style_expression_bodied_operators = when_on_single_line
+
+# CA1031: Do not catch general exception types
+dotnet_diagnostic.CA1031.severity = none
+
+# CA2007: Consider calling ConfigureAwait on the awaited task
+dotnet_diagnostic.CA2007.severity = silent
+
+# CA1003: Use generic event handler instances
+dotnet_diagnostic.CA1003.severity = suggestion
+
+# CA1056: URI-like properties should not be strings
+dotnet_diagnostic.CA1056.severity = suggestion
+
+# CA1819: Properties should not return arrays
+dotnet_diagnostic.CA1819.severity = suggestion
+
+# CA1724: Type names should not match namespaces
+dotnet_diagnostic.CA1724.severity = suggestion
+
+# CA1030: Use events where appropriate
+dotnet_diagnostic.CA1030.severity = silent
+
+# CA1002: Do not expose generic lists
+dotnet_diagnostic.CA1002.severity = suggestion
+
+# CA2227: Collection properties should be read only
+dotnet_diagnostic.CA2227.severity = suggestion
+
+# CA1014: Mark assemblies with CLSCompliant
+dotnet_diagnostic.CA1014.severity = none
+
+# CA1711: Identifiers should not have incorrect suffix
+dotnet_diagnostic.CA1711.severity = suggestion
+
+# CA1028: Enum Storage should be Int32
+dotnet_diagnostic.CA1028.severity = none
+
+# CA1826: Don't apply to ordefault methods
+dotnet_code_quality.CA1826.exclude_ordefault_methods = true
+
+# CA1308: Normalize strings to uppercase
+dotnet_diagnostic.CA1308.severity = none
+
+# CA1032: Implement standard exception constructors
+dotnet_diagnostic.CA1032.severity = none
+
+# C0162 exclude extension methods from needing null contract assertions
+dotnet_code_quality.CA1062.exclude_extension_method_this_parameter = true
+
+# CA2225: Operator overloads have named alternates
+dotnet_diagnostic.CA2225.severity = none
+
+# CA2229: Implement serialization constructors
+dotnet_diagnostic.CA2229.severity = none
+
+# CA1508: Avoid dead conditional code - Too many false positives for this to be useful.
+dotnet_diagnostic.CA1508.severity = none
+
+# SA1309: Field names should not begin with underscore
+dotnet_diagnostic.SA1309.severity = none
+
+# SA1101: Prefix local calls with this
+dotnet_diagnostic.SA1101.severity = none
+
+# SA1600: Elements should be documented
+dotnet_diagnostic.SA1600.severity = none
+
+# SA1623: Property summary documentation should match accessors
+dotnet_diagnostic.SA1623.severity = none
+
+# SA1200: Using directives should be placed correctly
+dotnet_diagnostic.SA1200.severity = none
+
+# SA1633: File should have header
+dotnet_diagnostic.SA1633.severity = none
+
+# SA1116: Split parameters should start on line after declaration
+dotnet_diagnostic.SA1116.severity = none
+
+# CA1303: Do not pass literals as localized parameters
+dotnet_diagnostic.CA1303.severity = none
+
+# SA1204: Static elements should appear before instance elements
+dotnet_diagnostic.SA1204.severity = none
+
+# CA1721: Property names should not match get methods
+dotnet_diagnostic.CA1721.severity = suggestion
+
+# CA1720: Identifier contains type name
+dotnet_diagnostic.CA1720.severity = none
+
+# SA1602: Enumeration items should be documented
+dotnet_diagnostic.SA1602.severity = suggestion
+
+# SA1642: Constructor summary documentation should begin with standard text
+dotnet_diagnostic.SA1642.severity = none
+
+# SA1611: Element parameters should be documented
+dotnet_diagnostic.SA1611.severity = none
+
+# SA1615: Element return value should be documented
+dotnet_diagnostic.SA1615.severity = none
+
+# SA1201: Elements should appear in the correct order
+dotnet_diagnostic.SA1201.severity = none
+
+# CA1054: URI-like parameters should not be strings
+dotnet_diagnostic.CA1054.severity = none
+
+# SA1515: Single-line comment should be preceded by blank line
+dotnet_diagnostic.SA1515.severity = none
+
+# CA1716: Identifiers should not match keywords
+dotnet_diagnostic.CA1716.severity = none
+
+# CA1812: Internal types should be instantiated (suppressing because analyser doesn't understand serialisers)
+dotnet_diagnostic.CA1812.severity = suggestion
+
+# CA1055: URI-like return values should not be strings
+dotnet_diagnostic.CA1055.severity = none
+
+# CA1062: Exclude our guard methods from null-check warnings.
+dotnet_code_quality.CA1062.null_check_validation_methods = NotNull
+
+dotnet_diagnostic.SA0001.severity=silent
+
+# CA2234: Pass system uri objects instead of strings
+dotnet_diagnostic.CA2234.severity = silent
+csharp_style_namespace_declarations=file_scoped:warning
diff --git a/.github/workflows/sdk-api-build.yml b/.github/workflows/sdk-api-build.yml
new file mode 100644
index 0000000..0af8a75
--- /dev/null
+++ b/.github/workflows/sdk-api-build.yml
@@ -0,0 +1,56 @@
+name: SDK API Build
+
+on:
+ push:
+ branches: [ develop, main ]
+ pull_request:
+ branches: [ develop ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Install GitVersion
+ uses: gittools/actions/gitversion/setup@v0.9.7
+ with:
+ versionSpec: '5.x'
+
+ - name: Determine Version
+ id: gitversion
+ uses: gittools/actions/gitversion/execute@v0.9.7
+
+ - name: Setup .NET 6 (SDK)
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: 6.0.x
+ source-url: https://nuget.pkg.github.com/enclave-networks/index.json
+ env:
+ NUGET_AUTH_TOKEN: ${{github.token}}
+
+ - name: Build
+ run: dotnet build Enclave.Sdk.Api.sln -c Release /p:Version=${{ steps.gitversion.outputs.SemVer }}
+
+ - name: Unit Tests
+ working-directory: tests/Enclave.Sdk.Api.Tests
+ run: dotnet test -c Release
+
+ - name: Push Github Source Packages
+ if: github.event_name == 'push'
+ run: dotnet nuget push src/**/*${{ steps.gitversion.outputs.SemVer }}.nupkg --api-key ${{github.token}} -s https://nuget.pkg.github.com/enclave-networks/index.json --skip-duplicate --no-symbols true
+
+ - name: Create Release
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: v${{ steps.gitversion.outputs.SemVer }}
+ release_name: Release v${{ steps.gitversion.outputs.SemVer }}
+ body: Latest SDK API Release
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index dfcfd56..4e3ee73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -348,3 +348,4 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
+tests/Enclave.Sdk.Api.Tests/IntegrationTests.cs
diff --git a/Enclave.Sdk.Api.sln b/Enclave.Sdk.Api.sln
new file mode 100644
index 0000000..d5378fa
--- /dev/null
+++ b/Enclave.Sdk.Api.sln
@@ -0,0 +1,49 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api", "src\Enclave.Sdk\Enclave.Sdk.Api.csproj", "{AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api.Tests", "tests\Enclave.Sdk.Api.Tests\Enclave.Sdk.Api.Tests.csproj", "{AC896965-74E9-405F-BA4E-E9D7336B906B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CA499537-7F6B-4E85-934A-04368AF78E9E}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{822BFA5E-EC93-45BF-B36E-1CAD1E7D88B1}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BD898880-875C-43EA-B22C-A7C801CFA607}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ GitVersion.yml = GitVersion.yml
+ .github\workflows\sdk-api-build.yml = .github\workflows\sdk-api-build.yml
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AC896965-74E9-405F-BA4E-E9D7336B906B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AC896965-74E9-405F-BA4E-E9D7336B906B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AC896965-74E9-405F-BA4E-E9D7336B906B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AC896965-74E9-405F-BA4E-E9D7336B906B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4} = {CA499537-7F6B-4E85-934A-04368AF78E9E}
+ {AC896965-74E9-405F-BA4E-E9D7336B906B} = {822BFA5E-EC93-45BF-B36E-1CAD1E7D88B1}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {33E7CCF5-8133-4138-BDDC-6B2785C7DDC6}
+ EndGlobalSection
+EndGlobal
diff --git a/GitVersion.yml b/GitVersion.yml
new file mode 100644
index 0000000..fc2356f
--- /dev/null
+++ b/GitVersion.yml
@@ -0,0 +1,11 @@
+assembly-versioning-scheme: None
+mode: ContinuousDelivery
+next-version: 0.0.1
+branches:
+ main:
+ mode: ContinuousDelivery
+ develop:
+ increment: Patch
+ignore:
+ sha: []
+merge-message-formats: {}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..ad6badf
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,17 @@
+
+
+
+ 9.0
+ https://github.com/enclave-networks/Enclave.Sdk.Api
+ enable
+ AllEnabledByDefault
+
+
+
+
+
+
+ All
+
+
+
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/AuthorityClient.cs b/src/Enclave.Sdk/Clients/AuthorityClient.cs
new file mode 100644
index 0000000..c413012
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/AuthorityClient.cs
@@ -0,0 +1,14 @@
+using Enclave.Sdk.Api.Clients.Interfaces;
+
+namespace Enclave.Sdk.Api.Clients;
+
+public class AuthorityClient : ClientBase, IAuthorityClient
+{
+ private string _orgRoute;
+
+ public AuthorityClient(HttpClient httpClient, string orgRoute)
+ : base(httpClient)
+ {
+ _orgRoute = orgRoute;
+ }
+}
diff --git a/src/Enclave.Sdk/Clients/ClientBase.cs b/src/Enclave.Sdk/Clients/ClientBase.cs
new file mode 100644
index 0000000..3afbd75
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/ClientBase.cs
@@ -0,0 +1,85 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Http.Json;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+
+namespace Enclave.Sdk.Api.Clients;
+
+///
+/// Base class used for commonly accessed methods and properties for all clients.
+///
+public abstract class ClientBase
+{
+ ///
+ /// HttpClient used for all clients API calls.
+ ///
+ protected HttpClient HttpClient { get; }
+
+ ///
+ /// Constructor to setup all required fields this is called by all child classes.
+ ///
+ /// HttpClient with baseUrl of the API used for all calls.
+ protected ClientBase(HttpClient httpClient)
+ {
+ HttpClient = httpClient;
+ }
+
+ ///
+ /// Get a string content for use with HttpClient.
+ ///
+ /// the object type to encode.
+ /// the object to encode.
+ /// String content of object.
+ /// throws if data provided is null.
+ protected StringContent CreateJsonContent(TModel data)
+ {
+ if (data is null)
+ {
+ throw new ArgumentNullException(nameof(data), "Data should not be null");
+ }
+
+ var json = JsonSerializer.Serialize(data, Constants.JsonSerializerOptions);
+ var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json);
+
+ return stringContent;
+ }
+
+ ///
+ /// Desreialize the httpContent.
+ ///
+ /// the object type to deserialise to.
+ /// httpContent from the API call.
+ /// the object of type specified.
+ protected async Task DeserialiseAsync(HttpContent httpContent)
+ {
+ if (httpContent is null)
+ {
+ return default;
+ }
+
+ return await httpContent.ReadFromJsonAsync(Constants.JsonSerializerOptions);
+ }
+
+ ///
+ /// Checks model is not null.
+ ///
+ /// Type being checked.
+ /// object being checked.
+ protected static void EnsureNotNull([NotNull] TModel? model)
+ {
+ if (model is null)
+ {
+ Throw();
+ }
+ }
+
+ ///
+ /// Throws an error every time it's called.
+ ///
+ /// Throws every time this is called.
+ [DoesNotReturn]
+ private static void Throw() =>
+ throw new InvalidOperationException("Return from API is null please ensure you've entered the correct data or raise an issue");
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/DNSClient.cs b/src/Enclave.Sdk/Clients/DNSClient.cs
new file mode 100644
index 0000000..73c8e42
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/DNSClient.cs
@@ -0,0 +1,14 @@
+using Enclave.Sdk.Api.Clients.Interfaces;
+
+namespace Enclave.Sdk.Api.Clients;
+
+public class DnsClient : ClientBase, IDnsClient
+{
+ private string _orgRoute;
+
+ public DnsClient(HttpClient httpClient, string orgRoute)
+ : base(httpClient)
+ {
+ _orgRoute = orgRoute;
+ }
+}
diff --git a/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs b/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs
new file mode 100644
index 0000000..bc98e38
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs
@@ -0,0 +1,14 @@
+using Enclave.Sdk.Api.Clients.Interfaces;
+
+namespace Enclave.Sdk.Api.Clients;
+
+public class EnrolmentKeysClient : ClientBase, IEnrolmentKeysClient
+{
+ private string _orgRoute;
+
+ public EnrolmentKeysClient(HttpClient httpClient, string orgRoute)
+ : base(httpClient)
+ {
+ _orgRoute = orgRoute;
+ }
+}
diff --git a/src/Enclave.Sdk/Clients/Interfaces/IAuthorityClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IAuthorityClient.cs
new file mode 100644
index 0000000..49b89d6
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/IAuthorityClient.cs
@@ -0,0 +1,5 @@
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+public interface IAuthorityClient
+{
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/Interfaces/IDnsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IDnsClient.cs
new file mode 100644
index 0000000..c409dd2
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/IDnsClient.cs
@@ -0,0 +1,5 @@
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+public interface IDnsClient
+{
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/Interfaces/IEnrolmentKeysClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IEnrolmentKeysClient.cs
new file mode 100644
index 0000000..a11d9cd
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/IEnrolmentKeysClient.cs
@@ -0,0 +1,5 @@
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+public interface IEnrolmentKeysClient
+{
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/Interfaces/ILogsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/ILogsClient.cs
new file mode 100644
index 0000000..11595fd
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/ILogsClient.cs
@@ -0,0 +1,5 @@
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+public interface ILogsClient
+{
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs
new file mode 100644
index 0000000..519151d
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs
@@ -0,0 +1,103 @@
+using Enclave.Sdk.Api.Data;
+using Enclave.Sdk.Api.Data.Account;
+using Enclave.Sdk.Api.Data.Organisations;
+using Enclave.Sdk.Api.Data.PatchModel;
+
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+///
+/// Provides access to organisation level API calls and organisation related clients.
+/// For more information please refer to our API docs at https://api.enclave.io/.
+///
+public interface IOrganisationClient
+{
+ ///
+ /// The organisation selected and the one used to create this client.
+ ///
+ AccountOrganisation Organisation { get; }
+
+ ///
+ /// An instance of associated with the current organisaiton.
+ ///
+ IAuthorityClient Authority { get; }
+
+ ///
+ /// An instance of associated with the current organisaiton.
+ ///
+ IDnsClient Dns { get; }
+
+ ///
+ /// An instance of associated with the current organisaiton.
+ ///
+ IEnrolmentKeysClient EnrolmentKeys { get; }
+
+ ///
+ /// An instance of associated with the current organisaiton.
+ ///
+ ILogsClient Logs { get; }
+
+ ///
+ /// An instance of associated with the current organisaiton.
+ ///
+ IPoliciesClient Policies { get; }
+
+ ///
+ /// An instance of associated with the current organisaiton.
+ ///
+ ISystemsClient Systems { get; }
+
+ ///
+ /// An instance of associated with the current organisaiton.
+ ///
+ ITagsClient Tags { get; }
+
+ ///
+ /// An instance of associated with the current organisaiton.
+ ///
+ IUnapprovedSystemsClient UnapprovedSystems { get; }
+
+ ///
+ /// Get more detail on your current organisaiton.
+ ///
+ /// A detailed organisation model.
+ Task GetAsync();
+
+ ///
+ /// Gets the users that have access to the current organisaiton.
+ ///
+ /// List of users associated with the current organisation.
+ Task> GetOrganisationUsersAsync();
+
+ ///
+ /// Get a list of invites that haven't been accepted.
+ ///
+ /// ReadOnlyList of pending invites.
+ Task> GetPendingInvitesAsync();
+
+ ///
+ /// Invite a user provided they haven't already been invited.
+ ///
+ /// Email address of the user you want to invite.
+ Task InviteUserAsync(string emailAddress);
+
+ ///
+ /// Cancel and invite before it's accepted.
+ ///
+ /// Email address of the user who's invite you want to revoke.
+ Task CancelInviteAync(string emailAddress);
+
+ ///
+ /// Removes a user from the organisation.
+ ///
+ /// The id of the users you want to remove.
+ Task RemoveUserAsync(string accountId);
+
+ ///
+ /// Patch request to update the organisation.
+ /// Use Builder.cs to help you generate the dictionary.
+ ///
+ /// An instance of used to setup our patch request.
+ /// The updated organisation.
+ /// Throws if builder is null.
+ Task UpdateAsync(PatchBuilder builder);
+}
diff --git a/src/Enclave.Sdk/Clients/Interfaces/IPoliciesClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IPoliciesClient.cs
new file mode 100644
index 0000000..b3283e4
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/IPoliciesClient.cs
@@ -0,0 +1,5 @@
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+public interface IPoliciesClient
+{
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/Interfaces/ISystemsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/ISystemsClient.cs
new file mode 100644
index 0000000..b0cf022
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/ISystemsClient.cs
@@ -0,0 +1,5 @@
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+public interface ISystemsClient
+{
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs
new file mode 100644
index 0000000..39647d1
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs
@@ -0,0 +1,20 @@
+using Enclave.Sdk.Api.Data.Pagination;
+using Enclave.Sdk.Api.Data.Tags;
+
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+///
+/// Access and search for tags.
+///
+public interface ITagsClient
+{
+ ///
+ /// Gets a paginated list of tags which can be searched and interated upon.
+ ///
+ /// A partial matching search term.
+ /// Sort order for the pagination.
+ /// Which page number do you want to return.
+ /// How many tags per page.
+ /// A paginated response model with links to get the previous, next, first and last pages.
+ Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null);
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/Interfaces/IUnapprovedSystemsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IUnapprovedSystemsClient.cs
new file mode 100644
index 0000000..aa34fdc
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/Interfaces/IUnapprovedSystemsClient.cs
@@ -0,0 +1,5 @@
+namespace Enclave.Sdk.Api.Clients.Interfaces;
+
+public interface IUnapprovedSystemsClient
+{
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/LogsClient.cs b/src/Enclave.Sdk/Clients/LogsClient.cs
new file mode 100644
index 0000000..53e7df1
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/LogsClient.cs
@@ -0,0 +1,14 @@
+using Enclave.Sdk.Api.Clients.Interfaces;
+
+namespace Enclave.Sdk.Api.Clients;
+
+public class LogsClient : ClientBase, ILogsClient
+{
+ private string _orgRoute;
+
+ public LogsClient(HttpClient httpClient, string orgRoute)
+ : base(httpClient)
+ {
+ _orgRoute = orgRoute;
+ }
+}
diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs
new file mode 100644
index 0000000..d876310
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs
@@ -0,0 +1,136 @@
+using System.Net.Http.Json;
+using Enclave.Sdk.Api.Clients.Interfaces;
+using Enclave.Sdk.Api.Data;
+using Enclave.Sdk.Api.Data.Account;
+using Enclave.Sdk.Api.Data.Organisations;
+using Enclave.Sdk.Api.Data.PatchModel;
+
+namespace Enclave.Sdk.Api.Clients;
+
+///
+public class OrganisationClient : ClientBase, IOrganisationClient
+{
+ private string _orgRoute;
+
+ ///
+ /// This constructor is called by when setting up the .
+ /// It also calls the constructor.
+ ///
+ /// an instance of httpClient with a baseURL referencing the API.
+ /// The current organisaiton used for routing the API calls.
+ public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrganisation)
+ : base(httpClient)
+ {
+ Organisation = currentOrganisation;
+ _orgRoute = $"org/{Organisation.OrgId}";
+ }
+
+ ///
+ public AccountOrganisation Organisation { get; }
+
+ ///
+ public IAuthorityClient Authority => throw new NotImplementedException();
+
+ ///
+ public IDnsClient Dns => throw new NotImplementedException();
+
+ ///
+ public IEnrolmentKeysClient EnrolmentKeys => throw new NotImplementedException();
+
+ ///
+ public ILogsClient Logs => throw new NotImplementedException();
+
+ ///
+ public IPoliciesClient Policies => throw new NotImplementedException();
+
+ ///
+ public ISystemsClient Systems => throw new NotImplementedException();
+
+ ///
+ public ITagsClient Tags => throw new NotImplementedException();
+
+ ///
+ public IUnapprovedSystemsClient UnapprovedSystems => throw new NotImplementedException();
+
+ ///
+ public async Task GetAsync()
+ {
+ var model = await HttpClient.GetFromJsonAsync(_orgRoute);
+
+ EnsureNotNull(model);
+
+ return model;
+ }
+
+ ///
+ public async Task UpdateAsync(PatchBuilder builder)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ using var encoded = CreateJsonContent(builder.Send());
+ var result = await HttpClient.PatchAsync(_orgRoute, encoded);
+
+ var model = await DeserialiseAsync(result.Content);
+
+ EnsureNotNull(model);
+
+ return model;
+ }
+
+ ///
+ public async Task> GetOrganisationUsersAsync()
+ {
+ var model = await HttpClient.GetFromJsonAsync($"{_orgRoute}/users");
+
+ EnsureNotNull(model);
+
+ return model.Users ?? Array.Empty();
+ }
+
+ ///
+ public async Task RemoveUserAsync(string accountId)
+ {
+ await HttpClient.DeleteAsync($"{_orgRoute}/users/{accountId}");
+ }
+
+ ///
+ public async Task> GetPendingInvitesAsync()
+ {
+ var model = await HttpClient.GetFromJsonAsync($"{_orgRoute}/invites");
+
+ EnsureNotNull(model);
+
+ return model.Invites ?? Array.Empty();
+ }
+
+ ///
+ public async Task InviteUserAsync(string emailAddress)
+ {
+ using var encoded = CreateJsonContent(new OrganisationInvite
+ {
+ EmailAddress = emailAddress,
+ });
+
+ var result = await HttpClient.PostAsync($"{_orgRoute}/invites", encoded);
+ }
+
+ ///
+ public async Task CancelInviteAync(string emailAddress)
+ {
+ using var encoded = CreateJsonContent(new OrganisationInvite
+ {
+ EmailAddress = emailAddress,
+ });
+
+ using var request = new HttpRequestMessage
+ {
+ Content = encoded,
+ Method = HttpMethod.Delete,
+ RequestUri = new Uri($"{HttpClient.BaseAddress}{_orgRoute}/invites"),
+ };
+
+ await HttpClient.SendAsync(request);
+ }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Clients/PoliciesClient.cs b/src/Enclave.Sdk/Clients/PoliciesClient.cs
new file mode 100644
index 0000000..0f6aafd
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/PoliciesClient.cs
@@ -0,0 +1,14 @@
+using Enclave.Sdk.Api.Clients.Interfaces;
+
+namespace Enclave.Sdk.Api.Clients;
+
+public class PoliciesClient : ClientBase, IPoliciesClient
+{
+ private string _orgRoute;
+
+ public PoliciesClient(HttpClient httpClient, string orgRoute)
+ : base(httpClient)
+ {
+ _orgRoute = orgRoute;
+ }
+}
diff --git a/src/Enclave.Sdk/Clients/SystemsClient.cs b/src/Enclave.Sdk/Clients/SystemsClient.cs
new file mode 100644
index 0000000..fb4bc02
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/SystemsClient.cs
@@ -0,0 +1,14 @@
+using Enclave.Sdk.Api.Clients.Interfaces;
+
+namespace Enclave.Sdk.Api.Clients;
+
+public class SystemsClient : ClientBase, ISystemsClient
+{
+ private string _orgRoute;
+
+ public SystemsClient(HttpClient httpClient, string orgRoute)
+ : base(httpClient)
+ {
+ _orgRoute = orgRoute;
+ }
+}
diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs
new file mode 100644
index 0000000..43eea63
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/TagsClient.cs
@@ -0,0 +1,62 @@
+using Enclave.Sdk.Api.Clients.Interfaces;
+using Enclave.Sdk.Api.Data.Pagination;
+using Enclave.Sdk.Api.Data.Tags;
+using System.Net.Http.Json;
+using System.Web;
+
+namespace Enclave.Sdk.Api.Clients;
+
+///
+public class TagsClient : ClientBase, ITagsClient
+{
+ private string _orgRoute;
+
+ ///
+ /// Consutructor which will be called by when it's created.
+ ///
+ /// an instance of httpClient with a baseURL referencing the API.
+ /// the orgRoute which specifies the orgId.
+ public TagsClient(HttpClient httpClient, string orgRoute)
+ : base(httpClient)
+ {
+ _orgRoute = orgRoute;
+ }
+
+ ///
+ public async Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null)
+ {
+ var queryString = BuildQueryString(searchTerm, sortOrder, pageNumber, perPage);
+
+ var model = await HttpClient.GetFromJsonAsync>($"{_orgRoute}/tags?{queryString}");
+
+ EnsureNotNull(model);
+
+ return model;
+ }
+
+ private static string? BuildQueryString(string? searchTerm, TagQuerySortOrder? sortOrder, int? pageNumber, int? perPage)
+ {
+ var queryString = HttpUtility.ParseQueryString(string.Empty);
+ if (searchTerm is not null)
+ {
+ queryString.Add("search", searchTerm);
+ }
+
+ if (sortOrder is not null)
+ {
+ queryString.Add("sort", sortOrder.ToString());
+ }
+
+ if (pageNumber is not null)
+ {
+ queryString.Add("page", pageNumber.ToString());
+ }
+
+ if (perPage is not null)
+ {
+ queryString.Add("per_page", perPage.ToString());
+ }
+
+ return queryString.ToString();
+ }
+}
diff --git a/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs
new file mode 100644
index 0000000..5eebb5f
--- /dev/null
+++ b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs
@@ -0,0 +1,14 @@
+using Enclave.Sdk.Api.Clients.Interfaces;
+
+namespace Enclave.Sdk.Api.Clients;
+
+public class UnapprovedSystemsClient : ClientBase, IUnapprovedSystemsClient
+{
+ private string _orgRoute;
+
+ public UnapprovedSystemsClient(HttpClient httpClient, string orgRoute)
+ : base(httpClient)
+ {
+ _orgRoute = orgRoute;
+ }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Constants.cs b/src/Enclave.Sdk/Constants.cs
new file mode 100644
index 0000000..b516ecd
--- /dev/null
+++ b/src/Enclave.Sdk/Constants.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace Enclave.Sdk.Api;
+
+internal static class Constants
+{
+ public static JsonSerializerOptions JsonSerializerOptions { get; } = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ public const string ApiUrl = "https://api.enclave.io";
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Account/AccountId.cs b/src/Enclave.Sdk/Data/Account/AccountId.cs
new file mode 100644
index 0000000..8cf6769
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Account/AccountId.cs
@@ -0,0 +1,11 @@
+using TypedIds;
+
+namespace Enclave.Sdk.Api.Data.Account;
+
+///
+/// AccountId struct to distinguish between Id types.
+///
+[TypedId]
+public readonly partial struct AccountId
+{
+}
diff --git a/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs
new file mode 100644
index 0000000..a0dd9bc
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs
@@ -0,0 +1,33 @@
+namespace Enclave.Sdk.Api.Data.Account;
+
+///
+/// Contains the role an account has within an organisation.
+///
+public class AccountOrganisation
+{
+ ///
+ /// The organisation ID.
+ ///
+ public OrganisationId OrgId { get; init; }
+
+ ///
+ /// The organisation name.
+ ///
+ public string OrgName { get; init; } = default!;
+
+ ///
+ /// The user's role within the organisation.
+ ///
+ public UserOrganisationRole Role { get; init; }
+}
+
+///
+/// Account orgs response model.
+///
+public class AccountOrganisationTopLevel
+{
+ ///
+ /// The set of organisations.
+ ///
+ public List Orgs { get; init; } = default!;
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Account/OrganisationId.cs b/src/Enclave.Sdk/Data/Account/OrganisationId.cs
new file mode 100644
index 0000000..5907c18
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Account/OrganisationId.cs
@@ -0,0 +1,11 @@
+using TypedIds;
+
+namespace Enclave.Sdk.Api.Data.Account;
+
+///
+/// OrganisaitonId struct to distinguish between Id types.
+///
+[TypedId]
+public readonly partial struct OrganisationId
+{
+}
diff --git a/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs b/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs
new file mode 100644
index 0000000..d5af46d
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs
@@ -0,0 +1,7 @@
+namespace Enclave.Sdk.Api.Data.Account;
+
+public enum UserOrganisationRole
+{
+ Owner,
+ Admin,
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Organisations/Enum/BillingEventLevel.cs b/src/Enclave.Sdk/Data/Organisations/Enum/BillingEventLevel.cs
new file mode 100644
index 0000000..7b92bd8
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Organisations/Enum/BillingEventLevel.cs
@@ -0,0 +1,22 @@
+namespace Enclave.Sdk.Api.Data.Organisations.Enum;
+
+///
+/// Defines levels for billing events.
+///
+public enum BillingEventLevel
+{
+ ///
+ /// Info.
+ ///
+ Information,
+
+ ///
+ /// Warning.
+ ///
+ Warning,
+
+ ///
+ /// Error.
+ ///
+ Error,
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Organisations/Enum/OrganisationPlan.cs b/src/Enclave.Sdk/Data/Organisations/Enum/OrganisationPlan.cs
new file mode 100644
index 0000000..8ed8025
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Organisations/Enum/OrganisationPlan.cs
@@ -0,0 +1,8 @@
+namespace Enclave.Sdk.Api.Data.Organisations.Enum;
+
+public enum OrganisationPlan
+{
+ Starter = 0,
+ Pro = 1,
+ Business = 2,
+}
diff --git a/src/Enclave.Sdk/Data/Organisations/Organisation.cs b/src/Enclave.Sdk/Data/Organisations/Organisation.cs
new file mode 100644
index 0000000..e329646
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Organisations/Organisation.cs
@@ -0,0 +1,55 @@
+using Enclave.Sdk.Api.Data.Account;
+using Enclave.Sdk.Api.Data.Organisations.Enum;
+
+namespace Enclave.Sdk.Api.Data.Organisations;
+
+///
+/// Organisation properties model.
+///
+public class Organisation
+{
+ ///
+ /// The organisation ID.
+ ///
+ public OrganisationId Id { get; init; }
+
+ ///
+ /// The UTC timestamp at which the organisation was created.
+ ///
+ public DateTime Created { get; init; }
+
+ ///
+ /// The name of the organisation.
+ ///
+ public string Name { get; init; } = default!;
+
+ ///
+ /// The current plan the organisation is on.
+ ///
+ public OrganisationPlan Plan { get; init; }
+
+ ///
+ /// The provided website info for the org (if known).
+ ///
+ public string? Website { get; init; }
+
+ ///
+ /// The provided phone info for the org (if known).
+ ///
+ public string? Phone { get; init; }
+
+ ///
+ /// The maximum number of systems this organisation can have enrolled.
+ ///
+ public int MaxSystems { get; init; }
+
+ ///
+ /// The number of enrolled systems.
+ ///
+ public long EnrolledSystems { get; init; }
+
+ ///
+ /// The number of systems waiting to be approved.
+ ///
+ public long UnapprovedSystems { get; init; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs
new file mode 100644
index 0000000..e54d139
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs
@@ -0,0 +1,24 @@
+using Enclave.Sdk.Api.Data.Organisations.Enum;
+
+namespace Enclave.Sdk.Api.Data.Organisations;
+
+///
+/// Defines the properties available for a billing event on an organisation.
+///
+public class OrganisationBillingEvent
+{
+ ///
+ /// The code indicating the billing event.
+ ///
+ public string Code { get; init; } = default!;
+
+ ///
+ /// A human-readable message describing the event.
+ ///
+ public string Message { get; init; } = default!;
+
+ ///
+ /// The event 'level'.
+ ///
+ public BillingEventLevel Level { get; init; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs
new file mode 100644
index 0000000..aa61746
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs
@@ -0,0 +1,12 @@
+namespace Enclave.Sdk.Api.Data.Organisations;
+
+///
+/// Invite model.
+///
+public class OrganisationInvite
+{
+ ///
+ /// The email address of the user to invite.
+ ///
+ public string EmailAddress { get; init; } = default!;
+}
diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs
new file mode 100644
index 0000000..a11b1eb
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs
@@ -0,0 +1,12 @@
+namespace Enclave.Sdk.Api.Data.Organisations;
+
+///
+/// Model for the pending list of org invites.
+///
+public class OrganisationPendingInvites
+{
+ ///
+ /// The set of outstanding invites that have been sent for this organisation.
+ ///
+ public IReadOnlyList? Invites { get; init; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs
new file mode 100644
index 0000000..59b7519
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs
@@ -0,0 +1,44 @@
+using Enclave.Sdk.Api.Data.Account;
+
+namespace Enclave.Sdk.Api.Data.Organisations;
+
+///
+/// Defines the properties of a user's membership of an organisation.
+///
+public class OrganisationUser
+{
+ ///
+ /// The account ID.
+ ///
+ public AccountId Id { get; init; }
+
+ ///
+ /// The user email address.
+ ///
+ public string? EmailAddress { get; init; }
+
+ ///
+ /// The user first name.
+ ///
+ public string? FirstName { get; init; }
+
+ ///
+ /// The user last name.
+ ///
+ public string? LastName { get; init; }
+
+ ///
+ /// The UTC timestamp for when the user joined the organisation.
+ ///
+ public DateTime JoinDate { get; init; }
+
+ ///
+ /// The user's role in the organisation.
+ ///
+ public UserOrganisationRole Role { get; init; }
+}
+
+public class OrganisationUsersTopLevel
+{
+ public IReadOnlyList? Users { get; init; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs b/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs
new file mode 100644
index 0000000..6018bb9
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs
@@ -0,0 +1,23 @@
+namespace Enclave.Sdk.Api.Data.Pagination;
+
+///
+/// Response model for paginated data.
+///
+/// The item type.
+public class PaginatedResponseModel
+{
+ ///
+ /// Metadata for the paginated data.
+ ///
+ public PaginationMetadata Metadata { get; init; }
+
+ ///
+ /// The related links for the current page of data.
+ ///
+ public PaginationLinks Links { get; init; }
+
+ ///
+ /// The requested page of items.
+ ///
+ public IEnumerable Items { get; init; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs b/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs
new file mode 100644
index 0000000..d72c346
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs
@@ -0,0 +1,27 @@
+namespace Enclave.Sdk.Api.Data.Pagination;
+
+///
+/// Defines the available pagination links.
+///
+public class PaginationLinks
+{
+ ///
+ /// The first page of data.
+ ///
+ public Uri First { get; init; }
+
+ ///
+ /// The previous page of data (or null if this is the first page).
+ ///
+ public Uri? Prev { get; init; }
+
+ ///
+ /// The next page of data (or null if this is the last page).
+ ///
+ public Uri? Next { get; init; }
+
+ ///
+ /// The last page of data.
+ ///
+ public Uri Last { get; init; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs b/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs
new file mode 100644
index 0000000..e66fee8
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs
@@ -0,0 +1,32 @@
+namespace Enclave.Sdk.Api.Data.Pagination;
+
+///
+/// Defines the metadata attached to a paginated response.
+///
+public class PaginationMetadata
+{
+ ///
+ /// The total number of items (across all pages).
+ ///
+ public int Total { get; init; }
+
+ ///
+ /// The first page number.
+ ///
+ public int FirstPage { get; init; }
+
+ ///
+ /// The previous page number (null if this is the first page).
+ ///
+ public int? PrevPage { get; init; }
+
+ ///
+ /// The last page number.
+ ///
+ public int LastPage { get; init; }
+
+ ///
+ /// The next page number (null if this is the last page).
+ ///
+ public int? NextPage { get; init; }
+}
diff --git a/src/Enclave.Sdk/Data/PatchBuilder.cs b/src/Enclave.Sdk/Data/PatchBuilder.cs
new file mode 100644
index 0000000..531a67e
--- /dev/null
+++ b/src/Enclave.Sdk/Data/PatchBuilder.cs
@@ -0,0 +1,66 @@
+using Enclave.Sdk.Api.Data.PatchModel;
+using System.Linq.Expressions;
+
+namespace Enclave.Sdk.Api.Data;
+
+///
+/// Class used to construct patch models.
+///
+/// The Type we're updating.
+public class PatchBuilder
+ where TModel : IPatchModel
+{
+ private Dictionary _patchDictionary = new Dictionary();
+
+ ///
+ /// Set a value in the global dictionary to the updated value.
+ ///
+ /// The type of the value you're updating.
+ /// Expression tree witht he property you want to update.
+ /// The new value.
+ /// Builder for fluent building.
+ /// Throws if either propExpr or newValue are null.
+ /// If the selected propExpr body is null.
+ public PatchBuilder Set(Expression> propExpr, TValue newValue)
+ {
+ if (newValue is null)
+ {
+ throw new ArgumentNullException(nameof(newValue), "please specificy a valid new value.");
+ }
+
+ if (propExpr is null)
+ {
+ throw new ArgumentNullException(nameof(propExpr), "please specificy a valid property expression.");
+ }
+
+ // Get the property expression.
+ var property = propExpr.Body as MemberExpression;
+
+ if (property is null)
+ {
+ throw new ArgumentException("Specified property expression is not valid.");
+ }
+
+ var propName = property.Member.Name;
+
+ _patchDictionary.Add(propName, newValue);
+
+ return this;
+ }
+
+ ///
+ /// Build the Dictionary and return it. then reset the dictionary so the builder can be reused.
+ ///
+ /// Dictionary built using the Set model in this class.
+ internal Dictionary Send()
+ {
+ try
+ {
+ return _patchDictionary;
+ }
+ finally
+ {
+ _patchDictionary = new Dictionary();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/PatchModel/AccountPatch.cs b/src/Enclave.Sdk/Data/PatchModel/AccountPatch.cs
new file mode 100644
index 0000000..c77b186
--- /dev/null
+++ b/src/Enclave.Sdk/Data/PatchModel/AccountPatch.cs
@@ -0,0 +1,22 @@
+namespace Enclave.Sdk.Api.Data.PatchModel;
+
+///
+/// Defines the modifiable properties of an account.
+///
+public class AccountPatch : IPatchModel
+{
+ ///
+ /// The user's declared first name.
+ ///
+ public string? FirstName { get; set; }
+
+ ///
+ /// The user's declared last name.
+ ///
+ public string? LastName { get; set; }
+
+ ///
+ /// Whether the user has indicated they would like to recieve marketing emails.
+ ///
+ public bool? EmailNotificationsEnabled { get; set; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/PatchModel/DnsRecordPatch.cs b/src/Enclave.Sdk/Data/PatchModel/DnsRecordPatch.cs
new file mode 100644
index 0000000..8412310
--- /dev/null
+++ b/src/Enclave.Sdk/Data/PatchModel/DnsRecordPatch.cs
@@ -0,0 +1,27 @@
+namespace Enclave.Sdk.Api.Data.PatchModel;
+
+///
+/// The patch model for a DNS record. Any values not provided will not be updated.
+///
+public class DnsRecordPatch : IPatchModel
+{
+ ///
+ /// The name of the record (without the zone).
+ ///
+ public string Name { get; set; } = default!;
+
+ ///
+ /// The set of tags to which this DNS name is applied.
+ ///
+ public IReadOnlyList? Tags { get; set; }
+
+ ///
+ /// The set of systems to which this DNS name is applied.
+ ///
+ public IReadOnlyList? Systems { get; set; }
+
+ ///
+ /// Any notes or additional info for this record.
+ ///
+ public string? Notes { get; set; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/PatchModel/DnsZonePatch.cs b/src/Enclave.Sdk/Data/PatchModel/DnsZonePatch.cs
new file mode 100644
index 0000000..32b9aeb
--- /dev/null
+++ b/src/Enclave.Sdk/Data/PatchModel/DnsZonePatch.cs
@@ -0,0 +1,17 @@
+namespace Enclave.Sdk.Api.Data.PatchModel;
+
+///
+/// Patch model for updating a zone.
+///
+public class DnsZonePatch : IPatchModel
+{
+ ///
+ /// The name of the zone; changing the name of a zone will change the fully-qualified name of every DNS record.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Update the notes for the zone.
+ ///
+ public string? Notes { get; set; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/PatchModel/IPatchModel.cs b/src/Enclave.Sdk/Data/PatchModel/IPatchModel.cs
new file mode 100644
index 0000000..96e0059
--- /dev/null
+++ b/src/Enclave.Sdk/Data/PatchModel/IPatchModel.cs
@@ -0,0 +1,5 @@
+namespace Enclave.Sdk.Api.Data.PatchModel;
+
+public interface IPatchModel
+{
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/PatchModel/OrganisationPatch.cs b/src/Enclave.Sdk/Data/PatchModel/OrganisationPatch.cs
new file mode 100644
index 0000000..ac3d8e5
--- /dev/null
+++ b/src/Enclave.Sdk/Data/PatchModel/OrganisationPatch.cs
@@ -0,0 +1,22 @@
+namespace Enclave.Sdk.Api.Data.PatchModel;
+
+///
+/// Model defining the modifiable properties for an organisation.
+///
+public class OrganisationPatch : IPatchModel
+{
+ ///
+ /// The org name.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// The org website.
+ ///
+ public string? Website { get; set; }
+
+ ///
+ /// A contact number for the organisation.
+ ///
+ public string? Phone { get; set; }
+}
diff --git a/src/Enclave.Sdk/Data/PatchModel/PolicyPatch.cs b/src/Enclave.Sdk/Data/PatchModel/PolicyPatch.cs
new file mode 100644
index 0000000..4cb526a
--- /dev/null
+++ b/src/Enclave.Sdk/Data/PatchModel/PolicyPatch.cs
@@ -0,0 +1,39 @@
+using Enclave.Sdk.Api.Data.Policy;
+
+namespace Enclave.Sdk.Api.Data.PatchModel;
+
+///
+/// Defines the modifiable properties of a policy.
+///
+public class PolicyPatch : IPatchModel
+{
+ ///
+ /// The policy name/description.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Whether or not the policy is enabled.
+ ///
+ public bool? IsEnabled { get; set; }
+
+ ///
+ /// A set of sender tags.
+ ///
+ public string[]? SenderTags { get; set; }
+
+ ///
+ /// A set of receiver tags.
+ ///
+ public string[]? ReceiverTags { get; set; }
+
+ ///
+ /// The set of ACLs for the policy.
+ ///
+ public PolicyAclModel[]? Acls { get; set; }
+
+ ///
+ /// Notes for the policy.
+ ///
+ public string? Notes { get; set; }
+}
diff --git a/src/Enclave.Sdk/Data/PatchModel/SystemPatch.cs b/src/Enclave.Sdk/Data/PatchModel/SystemPatch.cs
new file mode 100644
index 0000000..e309b1a
--- /dev/null
+++ b/src/Enclave.Sdk/Data/PatchModel/SystemPatch.cs
@@ -0,0 +1,27 @@
+namespace Enclave.Sdk.Api.Data.PatchModel;
+
+///
+/// Defines the modifiable properties of a system.
+///
+public class SystemPatch : IPatchModel
+{
+ ///
+ /// The system description.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Whether or not the system is enabled (and available for use).
+ ///
+ public bool? IsEnabled { get; set; }
+
+ ///
+ /// The set of tags applied to the system.
+ ///
+ public string[]? Tags { get; set; }
+
+ ///
+ /// Any notes or additional info for this system.
+ ///
+ public string? Notes { get; set; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Policy/PolicyAclModel.cs b/src/Enclave.Sdk/Data/Policy/PolicyAclModel.cs
new file mode 100644
index 0000000..95ec7e5
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Policy/PolicyAclModel.cs
@@ -0,0 +1,22 @@
+namespace Enclave.Sdk.Api.Data.Policy;
+
+///
+/// Model representing a single ACL entry for a policy.
+///
+public class PolicyAclModel
+{
+ ///
+ /// The protocol.
+ ///
+ public PolicyAclProtocol Protocol { get; set; }
+
+ ///
+ /// The port range (or single port) for the ACL.
+ ///
+ public string? Ports { get; set; }
+
+ ///
+ /// An optional description.
+ ///
+ public string? Description { get; set; }
+}
diff --git a/src/Enclave.Sdk/Data/Policy/PolicyAclProtocol.cs b/src/Enclave.Sdk/Data/Policy/PolicyAclProtocol.cs
new file mode 100644
index 0000000..aeba45d
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Policy/PolicyAclProtocol.cs
@@ -0,0 +1,9 @@
+namespace Enclave.Sdk.Api.Data.Policy;
+
+public enum PolicyAclProtocol
+{
+ Any,
+ Tcp,
+ Udp,
+ Icmp
+}
diff --git a/src/Enclave.Sdk/Data/Tags/TagItem.cs b/src/Enclave.Sdk/Data/Tags/TagItem.cs
new file mode 100644
index 0000000..a9962d6
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Tags/TagItem.cs
@@ -0,0 +1,37 @@
+namespace Enclave.Sdk.Api.Data.Tags;
+
+///
+/// Represents a single tag.
+///
+public class TagItem
+{
+ ///
+ /// The tag name.
+ ///
+ public string Tag { get; init; } = default!;
+
+ ///
+ /// The last time this tag was last referenced.
+ ///
+ public DateTime LastReferenced { get; init; }
+
+ ///
+ /// The number of systems that reference this tag.
+ ///
+ public int Systems { get; init; }
+
+ ///
+ /// The number of enrolment keys that reference this tag.
+ ///
+ public int Keys { get; init; }
+
+ ///
+ /// The number of policies that reference this tag.
+ ///
+ public int Policies { get; init; }
+
+ ///
+ /// The number of DNS records that reference this tag.
+ ///
+ public int DnsRecords { get; init; }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Data/Tags/TagQuerySortOrder.cs b/src/Enclave.Sdk/Data/Tags/TagQuerySortOrder.cs
new file mode 100644
index 0000000..d44057f
--- /dev/null
+++ b/src/Enclave.Sdk/Data/Tags/TagQuerySortOrder.cs
@@ -0,0 +1,8 @@
+namespace Enclave.Sdk.Api.Data.Tags;
+
+public enum TagQuerySortOrder
+{
+ Alphabetical,
+ RecentlyUsed,
+ ReferencedSystems,
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj
new file mode 100644
index 0000000..3e9595c
--- /dev/null
+++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj
@@ -0,0 +1,24 @@
+
+
+
+ 10.0
+ net6.0
+ Library
+ enable
+ enable
+ Enclave Networks Limited
+ True
+ True
+ false
+ README.md
+
+ Provides a NuGet package that makes it easier to consume the Enclave Management APIs.
+
+
+
+
+
+
+
+
+
diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs
new file mode 100644
index 0000000..f4972b0
--- /dev/null
+++ b/src/Enclave.Sdk/EnclaveClient.cs
@@ -0,0 +1,124 @@
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Reflection;
+using System.Text.Json;
+using Enclave.Sdk.Api.Clients;
+using Enclave.Sdk.Api.Clients.Interfaces;
+using Enclave.Sdk.Api.Data;
+using Enclave.Sdk.Api.Data.Account;
+using Enclave.Sdk.Api.Handlers;
+
+namespace Enclave.Sdk.Api;
+
+///
+/// Our main entry point for all API work.
+///
+public class EnclaveClient
+{
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// Create an using settings found in the .enclave/credentials.json file in your user directory.
+ ///
+ /// Throws if options in file are null.
+ public EnclaveClient()
+ {
+ var options = GetSettingsFile();
+
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _httpClient = SetupHttpClient(options);
+ }
+
+ ///
+ /// Simple Setup using just a PersonalAccessToken.
+ ///
+ /// The token created on the Enclave Portal.
+ public EnclaveClient(string personalAccessToken)
+ {
+ var options = new EnclaveClientOptions { PersonalAccessToken = personalAccessToken };
+
+ _httpClient = SetupHttpClient(options);
+ }
+
+ ///
+ /// Setup all requirements for making API calls.
+ ///
+ /// Options for setting up the .
+ /// Throws if options are null.
+ public EnclaveClient(EnclaveClientOptions options)
+ {
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _httpClient = SetupHttpClient(options);
+ }
+
+ ///
+ /// Gets a list of associated to the authorised user.
+ ///
+ /// List of organisation containing the OrgId and Name and the users role in that organisation.
+ /// throws when the Api returns a null response.
+ public async Task> GetOrganisationsAsync()
+ {
+ var organisations = await _httpClient.GetFromJsonAsync("/account/orgs", Constants.JsonSerializerOptions);
+
+ if (organisations is null)
+ {
+ throw new InvalidOperationException("Could not deserialize orgs associated to this token");
+ }
+
+ return organisations.Orgs;
+ }
+
+ ///
+ /// Create an from an .
+ ///
+ /// the from .
+ /// OrganisationClient that can be used for all following Api Calls related to an organisation.
+ public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisation)
+ {
+ return new OrganisationClient(_httpClient, organisation);
+ }
+
+ private EnclaveClientOptions? GetSettingsFile()
+ {
+ var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+ var location = Path.Combine(userProfile, ".enclave", "credentials.json");
+
+ try
+ {
+ var json = File.ReadAllText(location);
+ var settings = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+
+ return settings;
+ }
+ catch (IOException)
+ {
+ return null;
+ }
+ }
+
+ private static HttpClient SetupHttpClient(EnclaveClientOptions options)
+ {
+ var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler())
+ {
+ BaseAddress = new Uri(options.BaseUrl),
+ };
+
+ if (!string.IsNullOrWhiteSpace(options.PersonalAccessToken))
+ {
+ httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.PersonalAccessToken);
+ }
+
+ var clientHeader = new ProductInfoHeaderValue("Enclave.Sdk.Api", Assembly.GetExecutingAssembly().GetName().Version?.ToString());
+ httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader);
+ return httpClient;
+ }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/EnclaveClientOptions.cs b/src/Enclave.Sdk/EnclaveClientOptions.cs
new file mode 100644
index 0000000..54c7d55
--- /dev/null
+++ b/src/Enclave.Sdk/EnclaveClientOptions.cs
@@ -0,0 +1,27 @@
+using System.Text.Json.Serialization;
+
+namespace Enclave.Sdk.Api;
+
+///
+/// A representation of options used when creating .
+///
+public class EnclaveClientOptions
+{
+ ///
+ /// Default constructor which sets the BaseURL to the production version of the Enclave API.
+ ///
+ public EnclaveClientOptions()
+ {
+ BaseUrl = Constants.ApiUrl;
+ }
+
+ ///
+ /// the Personal access token from the Enclave portal.
+ ///
+ public string? PersonalAccessToken { get; set; }
+
+ ///
+ /// The base URL of the Enclave API endpoint.
+ ///
+ public string BaseUrl { get; set; }
+}
diff --git a/src/Enclave.Sdk/Exceptions/ProblemDetails.cs b/src/Enclave.Sdk/Exceptions/ProblemDetails.cs
new file mode 100644
index 0000000..26f044a
--- /dev/null
+++ b/src/Enclave.Sdk/Exceptions/ProblemDetails.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Enclave.Sdk.Api.Exceptions;
+
+///
+/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
+///
+[JsonConverter(typeof(ProblemDetailsJsonConverter))]
+public class ProblemDetails
+{
+ ///
+ /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
+ /// dereferenced, it provide human-readable documentation for the problem type
+ /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
+ /// "about:blank".
+ ///
+ [JsonPropertyName("type")]
+ public string? Type { get; set; }
+
+ ///
+ /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence
+ /// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
+ /// see[RFC7231], Section 3.4).
+ ///
+ [JsonPropertyName("title")]
+ public string? Title { get; set; }
+
+ ///
+ /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
+ ///
+ [JsonPropertyName("status")]
+ public int? Status { get; set; }
+
+ ///
+ /// A human-readable explanation specific to this occurrence of the problem.
+ ///
+ [JsonPropertyName("detail")]
+ public string? Detail { get; set; }
+
+ ///
+ /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
+ ///
+ [JsonPropertyName("instance")]
+ public string? Instance { get; set; }
+
+ ///
+ /// Gets the for extension members.
+ ///
+ /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as
+ /// other members of a problem type.
+ ///
+ ///
+ ///
+ /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters.
+ /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters.
+ ///
+ [JsonExtensionData]
+ public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal);
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Exceptions/ProblemDetailsException.cs b/src/Enclave.Sdk/Exceptions/ProblemDetailsException.cs
new file mode 100644
index 0000000..af74bfc
--- /dev/null
+++ b/src/Enclave.Sdk/Exceptions/ProblemDetailsException.cs
@@ -0,0 +1,15 @@
+namespace Enclave.Sdk.Api.Exceptions;
+
+internal class ProblemDetailsException : Exception
+{
+ public ProblemDetails ProblemDetails { get; }
+
+ public HttpResponseMessage Response { get; }
+
+ public ProblemDetailsException(ProblemDetails problemDetails, HttpResponseMessage response)
+ : base(problemDetails.Title)
+ {
+ ProblemDetails = problemDetails;
+ Response = response;
+ }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Exceptions/ProblemDetailsJsonConverter.cs b/src/Enclave.Sdk/Exceptions/ProblemDetailsJsonConverter.cs
new file mode 100644
index 0000000..380c8ae
--- /dev/null
+++ b/src/Enclave.Sdk/Exceptions/ProblemDetailsJsonConverter.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Enclave.Sdk.Api.Exceptions;
+
+internal sealed class ProblemDetailsJsonConverter : JsonConverter
+{
+ private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type");
+ private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title");
+ private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status");
+ private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail");
+ private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance");
+
+ public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var problemDetails = new ProblemDetails();
+
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException("Unexcepted end when reading JSON.");
+ }
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ ReadValue(ref reader, problemDetails, options);
+ }
+
+ if (reader.TokenType != JsonTokenType.EndObject)
+ {
+ throw new JsonException("Unexcepted end when reading JSON.");
+ }
+
+ return problemDetails;
+ }
+
+ public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ WriteProblemDetails(writer, value, options);
+ writer.WriteEndObject();
+ }
+
+ internal static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options)
+ {
+ if (TryReadStringProperty(ref reader, Type, out var propertyValue))
+ {
+ value.Type = propertyValue;
+ }
+ else if (TryReadStringProperty(ref reader, Title, out propertyValue))
+ {
+ value.Title = propertyValue;
+ }
+ else if (TryReadStringProperty(ref reader, Detail, out propertyValue))
+ {
+ value.Detail = propertyValue;
+ }
+ else if (TryReadStringProperty(ref reader, Instance, out propertyValue))
+ {
+ value.Instance = propertyValue;
+ }
+ else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes))
+ {
+ reader.Read();
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ // Nothing to do here.
+ }
+ else
+ {
+ value.Status = reader.GetInt32();
+ }
+ }
+ else
+ {
+ var key = reader.GetString()!;
+ reader.Read();
+ value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof(object), options);
+ }
+ }
+
+ internal static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, [NotNullWhen(true)] out string? value)
+ {
+ if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes))
+ {
+ value = default;
+ return false;
+ }
+
+ reader.Read();
+ value = reader.GetString()!;
+ return true;
+ }
+
+ internal static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options)
+ {
+ if (value.Type != null)
+ {
+ writer.WriteString(Type, value.Type);
+ }
+
+ if (value.Title != null)
+ {
+ writer.WriteString(Title, value.Title);
+ }
+
+ if (value.Status != null)
+ {
+ writer.WriteNumber(Status, value.Status.Value);
+ }
+
+ if (value.Detail != null)
+ {
+ writer.WriteString(Detail, value.Detail);
+ }
+
+ if (value.Instance != null)
+ {
+ writer.WriteString(Instance, value.Instance);
+ }
+
+ foreach (var kvp in value.Extensions)
+ {
+ writer.WritePropertyName(kvp.Key);
+ JsonSerializer.Serialize(writer, kvp.Value, kvp.Value?.GetType() ?? typeof(object), options);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/Handlers/ProblemDetailsHttpMessageHandler.cs b/src/Enclave.Sdk/Handlers/ProblemDetailsHttpMessageHandler.cs
new file mode 100644
index 0000000..5bbedfb
--- /dev/null
+++ b/src/Enclave.Sdk/Handlers/ProblemDetailsHttpMessageHandler.cs
@@ -0,0 +1,26 @@
+using System.Net.Http.Json;
+using Enclave.Sdk.Api.Exceptions;
+
+namespace Enclave.Sdk.Api.Handlers;
+
+internal sealed class ProblemDetailsHttpMessageHandler : DelegatingHandler
+{
+ public ProblemDetailsHttpMessageHandler()
+#pragma warning disable CA2000 // Dispose objects before losing scope
+ : base(new HttpClientHandler()) { }
+#pragma warning restore CA2000 // Dispose objects before losing scope
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct)
+ {
+ var response = await base.SendAsync(request, ct);
+
+ var mediaType = response.Content.Headers.ContentType?.MediaType;
+ if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.OrdinalIgnoreCase))
+ {
+ var problemDetails = await response.Content.ReadFromJsonAsync(Constants.JsonSerializerOptions, ct) ?? new ProblemDetails();
+ throw new ProblemDetailsException(problemDetails, response);
+ }
+
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/src/Enclave.Sdk/credentials-example.json b/src/Enclave.Sdk/credentials-example.json
new file mode 100644
index 0000000..0497e09
--- /dev/null
+++ b/src/Enclave.Sdk/credentials-example.json
@@ -0,0 +1,4 @@
+{
+ "personalAccessToken": "",
+ "baseUrl": ""
+}
\ No newline at end of file
diff --git a/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs b/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs
new file mode 100644
index 0000000..1fae9a2
--- /dev/null
+++ b/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs
@@ -0,0 +1,294 @@
+using System.Text.Json;
+using Enclave.Sdk.Api.Clients;
+using Enclave.Sdk.Api.Data;
+using Enclave.Sdk.Api.Data.Account;
+using Enclave.Sdk.Api.Data.Organisations;
+using Enclave.Sdk.Api.Data.PatchModel;
+using FluentAssertions;
+using NUnit.Framework;
+using WireMock.FluentAssertions;
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using WireMock.Server;
+
+namespace Enclave.Sdk.Api.Tests.Clients;
+
+public class OrganisationClientTests
+{
+ private OrganisationClient _organisationClient;
+ private WireMockServer _server;
+ private string _orgRoute;
+ private JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ [SetUp]
+ public void Setup()
+ {
+ _server = WireMockServer.Start();
+
+ var httpClient = new HttpClient
+ {
+ BaseAddress = new Uri(_server.Urls[0]),
+ };
+
+ var currentOrganisation = new AccountOrganisation
+ {
+ OrgId = OrganisationId.New(),
+ OrgName = "TestName",
+ Role = UserOrganisationRole.Admin,
+ };
+
+ _orgRoute = $"/org/{currentOrganisation.OrgId}";
+
+ _organisationClient = new OrganisationClient(httpClient, currentOrganisation);
+ }
+
+ [Test]
+ public async Task Should_return_a_detailed_organisation_model_when_calling_GetAsync()
+ {
+ // Arrange
+ var org = new Organisation
+ {
+ Id = OrganisationId.New(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath(_orgRoute).UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess()
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(JsonSerializer.Serialize(org, _serializerOptions)));
+
+ // Act
+ var result = await _organisationClient.GetAsync();
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Id.Should().Be(org.Id);
+ }
+
+ [Test]
+ public async Task Should_return_a_detailed_organisation_model_when_updating_with_UpdateAsync()
+ {
+ // Arrange
+ var org = new Organisation
+ {
+ Id = OrganisationId.New(),
+ Website = "newWebsite",
+ };
+
+ _server
+ .Given(Request.Create().WithPath(_orgRoute).UsingPatch())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess()
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(JsonSerializer.Serialize(org, _serializerOptions)));
+
+ var patchModel = new PatchBuilder().Set(x => x.Website, "newWebsite");
+
+ // Act
+ var result = await _organisationClient.UpdateAsync(patchModel);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Website.Should().Be(org.Website);
+ }
+
+ [Test]
+ public async Task Should_make_a_call_to_api_when_updating_with_UpdateAsync()
+ {
+ // Arrange
+ var org = new Organisation
+ {
+ Id = OrganisationId.New(),
+ Website = "newWebsite",
+ };
+
+ _server
+ .Given(Request.Create().WithPath(_orgRoute).UsingPatch())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess()
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(JsonSerializer.Serialize(org, _serializerOptions)));
+
+ var patchModel = new PatchBuilder().Set(x => x.Website, "newWebsite");
+
+ // Act
+ var result = await _organisationClient.UpdateAsync(patchModel);
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}");
+ }
+
+ [Test]
+ public async Task Should_return_a_list_of_organisation_users_when_calling_GetOrganisationUsersAsync()
+ {
+ // Arrange
+ var orgUsers = new OrganisationUsersTopLevel
+ {
+ Users = new List()
+ {
+ new OrganisationUser
+ {
+ Id = AccountId.New(),
+ FirstName = "testUser1",
+ LastName = "lastName1",
+ },
+ new OrganisationUser
+ {
+ Id = AccountId.New(),
+ FirstName = "testUser2",
+ LastName = "lastName2",
+ },
+ },
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/users").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess()
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(JsonSerializer.Serialize(orgUsers, _serializerOptions)));
+
+ // Act
+ var result = await _organisationClient.GetOrganisationUsersAsync();
+
+ // Assert
+ result.Count.Should().Be(2);
+ }
+
+ [Test]
+ public async Task Should_make_a_call_to_api_when_calling_GetOrganisationUsersAsync()
+ {
+ // Arrange
+ var orgUsers = new OrganisationUsersTopLevel
+ {
+ Users = new List(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/users").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess()
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(JsonSerializer.Serialize(orgUsers, _serializerOptions)));
+
+ // Act
+ var result = await _organisationClient.GetOrganisationUsersAsync();
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/users");
+ }
+
+ [Test]
+ public async Task Should_make_a_call_to_api_when_calling_RemoveUserAsync()
+ {
+ // Arrange
+ var accountId = "test";
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/users/{accountId}").UsingDelete())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess());
+ // Act
+ await _organisationClient.RemoveUserAsync(accountId);
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/users/{accountId}");
+ }
+
+ [Test]
+ public async Task Should_return_list_of_pending_invites_when_calling_GetPendingInvitesAsync()
+ {
+ // Arrange
+ var invites = new OrganisationPendingInvites()
+ {
+ Invites = new List
+ {
+ new OrganisationInvite { EmailAddress = "testEmail1" },
+ new OrganisationInvite { EmailAddress = "testEmail2" },
+ },
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/invites").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess()
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(JsonSerializer.Serialize(invites, _serializerOptions)));
+
+ // Act
+ var result = await _organisationClient.GetPendingInvitesAsync();
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Count.Should().Be(2);
+ }
+
+ [Test]
+ public async Task Should_make_a_call_to_api_when_calling_GetPendingInvitesAsync()
+ {
+ // Arrange
+ var invites = new OrganisationPendingInvites()
+ {
+ Invites = new List(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/invites").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess()
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(JsonSerializer.Serialize(invites, _serializerOptions)));
+
+ // Act
+ var result = await _organisationClient.GetPendingInvitesAsync();
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/invites");
+ }
+
+ [Test]
+ public async Task Should_make_a_call_to_api_when_calling_InviteUserAsync()
+ {
+ // Arrange
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/invites").UsingPost())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess());
+
+ // Act
+ await _organisationClient.InviteUserAsync("testEmailAddress");
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/invites");
+ }
+
+ [Test]
+ public async Task Should_make_a_call_to_api_when_calling_CancelInviteAync()
+ {
+ // Arrange
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/invites").UsingDelete())
+ .RespondWith(
+ Response.Create()
+ .WithSuccess());
+
+ // Act
+ await _organisationClient.CancelInviteAync("testEmailAddress");
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/invites");
+ }
+}
diff --git a/tests/Enclave.Sdk.Api.Tests/Clients/TagClientTests.cs b/tests/Enclave.Sdk.Api.Tests/Clients/TagClientTests.cs
new file mode 100644
index 0000000..5675c01
--- /dev/null
+++ b/tests/Enclave.Sdk.Api.Tests/Clients/TagClientTests.cs
@@ -0,0 +1,209 @@
+using System.Text.Json;
+using Enclave.Sdk.Api.Clients;
+using Enclave.Sdk.Api.Data.Pagination;
+using Enclave.Sdk.Api.Data.Tags;
+using FluentAssertions;
+using NUnit.Framework;
+using WireMock.FluentAssertions;
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using WireMock.Server;
+
+namespace Enclave.Sdk.Api.Tests.Clients;
+
+public class TagClientTests
+{
+ private TagsClient _tagClient;
+ private WireMockServer _server;
+ private string _orgRoute;
+ private JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ [SetUp]
+ public void Setup()
+ {
+ _server = WireMockServer.Start();
+
+ var httpClient = new HttpClient
+ {
+ BaseAddress = new Uri(_server.Urls[0]),
+ };
+
+ var orgId = "testId";
+
+ _orgRoute = $"/org/{orgId}";
+
+ _tagClient = new TagsClient(httpClient, _orgRoute);
+ }
+
+ [Test]
+ public async Task Should_return_a_list_of_tags_in_pagination_format()
+ {
+ // Arrange
+ var responseModel = new PaginatedResponseModel
+ {
+ Items = new List
+ {
+ new TagItem { Tag = "tag1", Keys = 12, DnsRecords = 1, Policies = 0, Systems = 3 },
+ new TagItem { Tag = "tag2", Keys = 13, DnsRecords = 0, Policies = 43, Systems = 0 },
+ },
+ Links = new PaginationLinks(),
+ Metadata = new PaginationMetadata(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithStatusCode(200)
+ .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions)));
+
+ // Act
+ var result = await _tagClient.GetAsync();
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Items.Should().NotBeNull();
+ }
+
+ [Test]
+ public async Task Should_make_call_to_api_with_search_queryString()
+ {
+ // Arrange
+ var searchTerm = "test";
+
+ var responseModel = new PaginatedResponseModel
+ {
+ Items = new List(),
+ Links = new PaginationLinks(),
+ Metadata = new PaginationMetadata(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithStatusCode(200)
+ .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions)));
+
+ // Act
+ var result = await _tagClient.GetAsync(searchTerm: searchTerm);
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?search={searchTerm}");
+ }
+
+ [Test]
+ public async Task Should_make_call_to_api_with_sort_queryString()
+ {
+ // Arrange
+ var sortEnum = TagQuerySortOrder.Alphabetical;
+
+ var responseModel = new PaginatedResponseModel
+ {
+ Items = new List(),
+ Links = new PaginationLinks(),
+ Metadata = new PaginationMetadata(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithStatusCode(200)
+ .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions)));
+
+ // Act
+ var result = await _tagClient.GetAsync(sortOrder: sortEnum);
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?sort={sortEnum}");
+ }
+
+ [Test]
+ public async Task Should_make_call_to_api_with_page_queryString()
+ {
+ // Arrange
+ var pageNumber = 1;
+
+ var responseModel = new PaginatedResponseModel
+ {
+ Items = new List(),
+ Links = new PaginationLinks(),
+ Metadata = new PaginationMetadata(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithStatusCode(200)
+ .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions)));
+
+ // Act
+ var result = await _tagClient.GetAsync(pageNumber: pageNumber);
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?page={pageNumber}");
+ }
+
+ [Test]
+ public async Task Should_make_call_to_api_with_per_page_queryString()
+ {
+ // Arrange
+ var perPage = 1;
+
+ var responseModel = new PaginatedResponseModel
+ {
+ Items = new List(),
+ Links = new PaginationLinks(),
+ Metadata = new PaginationMetadata(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithStatusCode(200)
+ .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions)));
+
+ // Act
+ var result = await _tagClient.GetAsync(perPage: perPage);
+
+ // Assert
+ _server.Should().HaveReceivedACall().AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?per_page={perPage}");
+ }
+
+ [Test]
+ public async Task Should_make_call_to_api_with_all_queryStrings()
+ {
+ // Arrange
+ var searchTerm = "test";
+ var sortEnum = TagQuerySortOrder.Alphabetical;
+ var perPage = 1;
+ var pageNumber = 1;
+
+ var responseModel = new PaginatedResponseModel
+ {
+ Items = new List(),
+ Links = new PaginationLinks(),
+ Metadata = new PaginationMetadata(),
+ };
+
+ _server
+ .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithStatusCode(200)
+ .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions)));
+
+ // Act
+ var result = await _tagClient.GetAsync(searchTerm: searchTerm, sortOrder: sortEnum, pageNumber: pageNumber, perPage: perPage);
+
+ // Assert
+ _server.Should().HaveReceivedACall()
+ .AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?search={searchTerm}&sort={sortEnum}&page={pageNumber}&per_page={perPage}");
+ }
+}
\ No newline at end of file
diff --git a/tests/Enclave.Sdk.Api.Tests/Enclave.Sdk.Api.Tests.csproj b/tests/Enclave.Sdk.Api.Tests/Enclave.Sdk.Api.Tests.csproj
new file mode 100644
index 0000000..149c637
--- /dev/null
+++ b/tests/Enclave.Sdk.Api.Tests/Enclave.Sdk.Api.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ 10.0
+ net6.0
+ enable
+ disable
+ 0c261991-ae7a-4598-9650-7f05d2939f98
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs b/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs
new file mode 100644
index 0000000..0091ac5
--- /dev/null
+++ b/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs
@@ -0,0 +1,80 @@
+using System.Text.Json;
+using Enclave.Sdk.Api.Data;
+using Enclave.Sdk.Api.Data.Account;
+using FluentAssertions;
+using NUnit.Framework;
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using WireMock.Server;
+
+namespace Enclave.Sdk.Api.Tests;
+
+public class EnclaveClientTests
+{
+ private EnclaveClient _client;
+ private WireMockServer _server;
+
+ private readonly JsonSerializerOptions _serializerOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ [SetUp]
+ public void Setup()
+ {
+ _server = WireMockServer.Start();
+
+ var enclaveSettings = new EnclaveClientOptions
+ {
+ BaseUrl = _server.Urls[0],
+ };
+
+ _client = new EnclaveClient(enclaveSettings);
+ }
+
+ [Test]
+ public async Task Should_return_list_of_orgs_when_calling_GetOrganisationsAsync()
+ {
+ // Arrange
+ var accountOrg = new AccountOrganisationTopLevel
+ {
+ Orgs = new List
+ {
+ new AccountOrganisation
+ {
+ OrgId = OrganisationId.New(),
+ OrgName = "TestName",
+ Role = UserOrganisationRole.Admin,
+ },
+ },
+ };
+
+ _server
+ .Given(Request.Create().WithPath("/account/orgs").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithStatusCode(200)
+ .WithBody(JsonSerializer.Serialize(accountOrg, _serializerOptions)));
+
+ // Act
+ var result = await _client.GetOrganisationsAsync();
+
+ // Assert
+ result.FirstOrDefault().OrgId.Should().Be(accountOrg.Orgs.FirstOrDefault().OrgId);
+ }
+
+ [Test]
+ public async Task Should_throw_invalid_operation_exception_if_response_does_not_contain_an_organisation_model()
+ {
+ // Arrange
+ _server
+ .Given(Request.Create().WithPath("/account/orgs").UsingGet())
+ .RespondWith(
+ Response.Create()
+ .WithStatusCode(200)
+ .WithBody("null"));
+
+ // Assert
+ await _client.Invoking(c => c.GetOrganisationsAsync()).Should().ThrowAsync();
+ }
+}
\ No newline at end of file
diff --git a/tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs b/tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs
new file mode 100644
index 0000000..aa8ddbd
--- /dev/null
+++ b/tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs
@@ -0,0 +1,9 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "We are using variables for tests they do not need to be disposed at end of scope", Scope = "module")]
+[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "unit test methods use underscores for readability", Scope = "module")]